Skip to content

OpenVINO: fix bool min/max and masked read lowering, and follow-ups to #24294 - #24396

Open
shoumikhin wants to merge 32 commits into
pytorch:mainfrom
shoumikhin:openvino-bool-minmax
Open

shoumikhin wants to merge 32 commits into
pytorch:mainfrom
shoumikhin:openvino-bool-minmax

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

What is wrong today

The OpenVINO partitioner gives min, max, amin, amax, minimum, maximum, clamp and sign to OpenVINO even when every tensor they read is bool. OpenVINO cannot convert these for bool:

ReduceMin ... Element type of data input must be numeric. Got: boolean

torch.min(x > 0, dim=1) and MultivariateNormal's argument check (PyTorch 2.15) hit this. Reading masked values after writing them fails too:

x[mask] = x[mask] * 2
return x + x[mask].sum()  # Dynamic rank for aten::index input is not supported.

What this change does

The partitioner leaves these ops to ExecuTorch's CPU kernels when every tensor they read is bool. Ops that also read a float tensor, like maximum(mask, x), stay with OpenVINO. Casting to uint8 could give wrong min(dim) and max(dim) indices on ties, as the docs now note.

For the masked read, preprocess fills each node's tensor_meta from val. OpenVINO reads ranks only from tensor_meta, which ExecuTorch leaves empty.

Follow-ups to #24294:

  • The test runner exits with 1 on failure (from Fix OpenVINO tests in CI #24305), and CI runs model tests even after an op test fails.
  • OpenVINO dropped a first input that only a folded cumsum read, failing every call. Preprocess puts it back and checks the input count.
  • An output planned as dynamic but compiled as fixed failed every call. Its buffer now starts with the compiled shape.
  • The cumsum fold works under a CUDA default device on a CPU-only PyTorch (exir's ScalarToTensorPass still fails there).
  • An empty last inference_precision entry now also means the default at load.
  • The docs say which CPUs compute some integer ops in float32, and where the float32 default is slower.

What was tested

Linux x86_64, OpenVINO 2026.4.1 and 2025.1.0, CPU, unless noted:

  • Op and model tests pass. The bool min model (both releases) and the masked read (2026.4.1) failed to lower, and now match eager.
  • On 2026.4.1, each backend fix has a test that fails without it. The runner and CI script were checked by hand.
  • Partitions are unchanged for every probed op with integer, float16, bfloat16 or float32 inputs, and for MobileNetV2, ResNet-18 and a transformer layer.
  • With the tensor_meta change, all other test partitions convert to the same OpenVINO graphs.
  • The docs' integer claims were measured on x86 through the delegate, and on ARM with plain OpenVINO 2026.4.1.

shoumikhin and others added 16 commits October 31, 2026 14:39
The OpenVINO CI job runs backends/openvino/tests/test_runner.py. When a test
failed, the runner printed "completed with failures" but still exited with
code 0, so the CI job stayed green and the failure went unnoticed.

Now the runner exits with code 1 when any test fails, so CI turns red.

Co-authored-by: suryasidd <surya.siddharth.pemmaraju@intel.com>
…d it

A position cumsum such as cumsum(ones_like(input_ids)) - 1 reads only the
shape of input_ids. Once FoldConstantCumsumPass folds it, nothing in the
partition reads input_ids. When that input comes first, OpenVINO's
PyTorch converter takes it for forward's self and removes it. The runtime
still passes every partition input, so every call failed with
"expected 2 args (inputs=1 + outputs=1), got 3". An unused input in any
other position is kept by the converter.

Not folding is no fix, because masks built from such a cumsum then go
wrong on OpenVINO's CPU plugin from the second call on. Instead, when the
converted model has exactly one parameter fewer than the partition has
inputs, preprocess puts the first one back, so the compiled model takes
the same inputs the delegate passes. The test leaves both the first input
and a later one unused.
ExecuTorch can plan an output with a dynamic size while OpenVINO
compiles a fixed shape for it, for example x[torch.ones(4, dtype=bool)]
once OpenVINO folds the mask. The buffer the runtime makes for such an
output started empty, and OpenVINO refuses an output tensor whose shape
differs from a fixed one, so every call failed with 0x1. Before the
owned buffers, these programs ran.

The buffer now starts with the shape of the infer request's own output
tensor: empty along each dynamic dimension and the full
shape where OpenVINO fixed it.
FoldConstantCumsumPass evaluates a full_like from an empty tensor of its
input's shape. That tensor went to the default device, so lowering for
OpenVINO under torch.device("cuda") or set_default_device("cuda") failed
on a CPU-only PyTorch with "Torch not compiled with CUDA enabled", and
on a CUDA build it used the GPU for nothing. The pass evaluates
everything else on the CPU already; this tensor now goes there too.
With inference_precision given twice, for example bf16 and then an
empty value, lowering keeps the last entry and so compiles with the
float32 default, but the runtime skipped empty entries and applied
bf16. On x86, where the runtime decides, the program then computed in
bfloat16. The runtime now takes the last entry as well, including an
empty one, which means the default.
Lowering passes the precision too, so the comment was wrong to say only
the runtime does. What makes the check depend on the runtime is that on
x86 OpenVINO applies only the precision it gets when it loads the model.
The cumsum fold lines had landed between the comment and the decoder
call it explains, so the comment read as if it were about the fold.
Since each cumsum is folded on its own, _evaluate is only ever asked for
one node. Taking and returning that one value drops the list and dict it
no longer needs, and a check that could never be false.
Only the attention mask test covered an int32 cumsum, and only through
an OpenVINO CPU bug that newer OpenVINO releases fix. A fold limited to
int64 now fails a pass-level test too.
Only x86 CPUs with bfloat16 units computed in bfloat16 before, so only
they get slower, and only there does asking for bf16 bring the earlier
speed back. On other x86 CPUs OpenVINO already computed in float32.
The precision docs said int64 results only go wrong outside the int32
range. Measured through the delegate on a CPU, an int64 sum is computed
in float32: 16777216 + 1 gives 16777216, and 123456789 + 987654321 gives
1111111168. cumsum and prod near 2^24 stay exact.
Only an output compiled with a fully fixed shape refuses a buffer of another shape. A partly dynamic output accepts an empty one. Comment only.
OpenVINO 2025.1 has the same CPU plugin bug with integer cumsums over constants as the later releases, so the fold's docstring now starts the range there. Docstring only.
…ioner

OpenVINO converts min, max, amin, amax, minimum, maximum, clamp and sign
to its ReduceMin, ReduceMax, Minimum, Maximum and Sign operations, and
none of those accept bool tensors. The partitioner still gave these ops
to OpenVINO when they read bool tensors, so the whole program failed to
lower:

    ReduceMin ... Element type of data input must be numeric. Got: boolean

With PyTorch 2.15, MultivariateNormal exports two such mins when it
checks scale_tril, so a model that builds one could not use OpenVINO.

The partitioner now leaves these ops to the CPU when every tensor they
read is bool, and the portable kernels compute them as PyTorch does. An
op with a bool and a float input, such as maximum(mask, x), is computed
in float, which OpenVINO converts, so it stays with OpenVINO. Other
dtypes are not affected.
create_owned_output reported errors through its return value and its
buffer through an out-parameter, and took a whole tensor only to read its
dtype. It now returns exr::Result<ov_tensor_t*> and takes the dtype, like
create_ov_tensor beside it, so its call site reads like the one for
create_ov_tensor again. No behavior change.
…_precision

The check that the runtime takes the last inference_precision entry ran
only on x86 CPUs with bfloat16 units, so CI could skip it. A new test
gives an invalid value and then an empty one. Lowering keeps only the
empty one, and the runtime has to as well, or it fails to load the
program. On the runtime before that fix it fails with "unsupported
inference_precision 'f7'".
@shoumikhin shoumikhin added the release notes: openvino OpenVino backend related issues, features, bugs etc. label Nov 1, 2026
Copilot AI balanced review requested due to automatic review settings November 1, 2026 02:19
@pytorch-bot

pytorch-bot Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24396

Note: Links to docs will display an error until the docs builds have been completed.

❌ 1 New Failure, 279 Pending, 6 Unrelated Failures, 2 Unclassified Failures

As of commit 75d6775 with merge base 05092c1 (image):

NEW FAILURE - The following job has failed:

UNCLASSIFIED FAILURES - DrCI could not classify the following jobs because the workflow did not run on the merge base. The failures may be pre-existing on trunk or introduced by this PR:

BROKEN TRUNK - The following jobs failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 1, 2026

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

@claude

claude Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 22m 44s —— View job


PR Review: #24396. I found no blockers, only minor points: the CI script now skips the model tests after an op failure, preprocess has no sanity check, two tests can't catch what they target, and it's unclear whether all ten bool ops need to leave OpenVINO.

Detailed review

Summary

The PR keeps bool min/max/amin/amax/minimum/maximum/clamp/sign on the CPU. It also carries the #24294 follow-ups: re-adding a dropped first input, sizing OpenVINO-owned output buffers to the compiled shape, making the cumsum fold tensor on the CPU, taking the last inference_precision, failing the test runner, and docs. No blockers. All findings below are minor.

Code Quality

  • backends/openvino/preprocess.py:114-121: The fix-up assumes input 0 was dropped based only on the parameter count, and never checks the result.
    • A count that is off in any other way still lowers. Every call then fails at the args check in OpenvinoBackend.cpp:475-482, which is the failure this PR fixes for one case.
    • If a converter release ever dropped a different unused input, the new parameter would go to index 0 and shift the rest. The runtime binds inputs by index (OpenvinoBackend.cpp:513), so inputs with the same type and shape would silently swap.
    • Fix: re-add the parameter only when the first placeholder of module.graph has no users, and raise if len(model.get_parameters()) != len(input_types) afterwards. Fix this →
  • backends/openvino/partitioner.py:36-41, 54 (low confidence: I did not check OpenVINO's support table or its clamp translator): aten.clamp.Tensor is not in bool_unsupported_ops, and _reads_only_bool assumes OpenVINO promotes mixed inputs. OpenVINO may instead claim clamp.Tensor and convert the bounds to the input's type (ConvertLike) before Maximum/Minimum. In that case torch.clamp(mask, min=float_tensor) would still hit the bool Maximum error, and so would clamp_min(mask, t) once it is decomposed. The fix would be to judge clamp by its first input only and add clamp.Tensor to the set.

Infrastructure

  • backends/openvino/tests/test_runner.py:75: .ci/scripts/test_openvino.sh runs under set -ex (line 8). With the new sys.exit(1), a failing op test ends the script before python test_runner.py --test_type models (line 16) starts. A failing CI run then reports nothing about the model tests, which used to run every time. Run both suites and exit non-zero at the end if either failed. Fix this →

Testing

  • backends/openvino/tests/ops/test_reductions.py:124-125: test_bool_ops_run_on_the_cpu returns only the .values of m.min(1) and m.max(1). The indices are the PR's reason for keeping these ops off OpenVINO, and rows 0 and 1 are all ties, but nothing compares the indices with eager. Moving the ops back into OpenVINO with a uint8 cast would not be caught. Return .indices too. Fix this →
  • backends/openvino/tests/ops/test_reductions.py:254: x is int64 in both subtests, so torch.cumsum(part, 0, dtype=torch.int32) + x is int64 whether the folded constant is int32 or int64. The new int32 subtest therefore can't catch a fold that drops dtype=. Give x the subtest's dtype so that assert_close also compares dtypes. Fix this →

API Design

  • backends/openvino/README.md:95-97 and docs/source/build-run-openvino.md:149-151 (low confidence: I did not check this against the OpenVINO source): the new sentence names only sum as computed in float32, which suggests the other integer reductions are exact. The CPU plugin's Reduce kernel may convert int32 values to float32 for every reduce mode. If so, amax, amin, max(), min() and prod round the same way above 2^24; for example, torch.tensor([16_777_217]).amax() would give 16,777,216. Please check amax and prod before the docs say only sum is affected.

Performance

  • backends/openvino/partitioner.py:51-62: The PR's reason for not casting inside OpenVINO is the unspecified tie order, but that only matters for the indices of min.dim/max.dim.
    • The other eight ops have no indices and an exact bool form. amin/min are all, amax/max are any, minimum/maximum are and/or, sign is the identity, and clamp with a float bound can cast its input to float first.
    • min.dim/max.dim don't need the CPU either when their indices are unused. The PR's own example reads only .values.
    • Sending all ten to the CPU splits the OpenVINO partition wherever one of them sits mid-graph, which adds a delegate boundary and a portable-kernel round trip.
    • A cast in preprocess would keep these ops in OpenVINO and leave only min.dim/max.dim with used indices on the CPU. _passes/cast_reduction_input_pass.py already does this for bool cumsum/prod.
    • This is not a regression, since these graphs failed to lower before.
  • backends/openvino/partitioner.py:109-110 (medium confidence: traced by hand, not run): getitem is reported as supported even when its producer is not. Now that bool min.dim/max.dim are rejected, this appears to split the test model into two delegates:
    • CapabilityBasedPartitioner's merge-all step puts the getitems of min.dim/max.dim into flip's partition.
    • gt then can't join without a cycle through min.dim.
    • The getitems are later moved back to their unpartitioned producers, leaving gt and flip as two delegates with a CPU round trip between them.
    • That matches expected_partitions=2 at test_reductions.py:140, and the test now locks the split in.
    • Reporting getitem as supported only when node.args[0] is should give one partition. The native backend already does this (backends/native/test/test_partitioner.py:99).
    • The getitem rule predates this PR, but this PR's change is what triggers it. Fix this →

Recommendation

Approve. Nothing blocks merging, and none of the findings is a regression. The CI script change, the preprocess check and the two test fixes are small enough to make in this PR. The two partitioning points can be follow-ups.

· [Branch: openvino-bool-minmax](https://github.com/pytorch/executorch/tree/openvino-bool-minmax)

The OpenVINO CI job runs the op tests and then the model tests under
set -e. Now that the test runner exits with code 1 when a test fails, a
failing op test stopped the job before the model tests started, so a red
run said nothing about the models. The script now runs both suites and
exits with code 1 if either failed.
Preprocess put the first partition input back whenever the converted
model had one parameter fewer than the partition has inputs. Now it does
so only when that first input is really unused, and lowering fails when
the counts still differ. The runtime binds inputs by position, so another
mismatch would have made every call fail, or swapped two inputs of the
same type and shape. A partition can also have no inputs at all, such as
a comparison that reads only a buffer next to a bool min on the CPU, and
a test covers that case.
TestBoolMinMax compared only the values of min(dim) and max(dim). Their
indices are where the order of ties matters, and the all-true and
all-false rows are all ties, so the test now compares the indices with
eager too. Its docstring also says more exactly why these ops leave
OpenVINO.
x was int64 in both subtests, so the result was int64 whatever dtype the
folded cumsum had, and a fold that dropped dtype=torch.int32 still
passed. x now has the subtest's dtype, so such a fold fails the int32
subtest.
The docs said only sum reduces integers in float32. Measured through the
delegate on a CPU with OpenVINO 2026.4.1 and 2025.1.0, amax, amin, max and
min of int32 and int64 tensors do too: amax over a tensor that holds
16,777,217 gives 16,777,216. Inputs and running totals round as well, so
the sum of 16,777,217 and -16,777,216 gives 0. cumsum and maximum stay
exact, and so does prod where OpenVINO computes it (2026.4.1).
Copilot AI balanced review requested due to automatic review settings November 1, 2026 05:37

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I pushed the small items:

  • .ci/scripts/test_openvino.sh now runs the model tests even after an op test fails, and still exits with code 1.
  • Preprocess puts the first input back only when that input is unused, and lowering fails if the input count still differs. A partition with no inputs at all, which a bool min on the CPU can leave behind, is covered by a new test.
  • TestBoolMinMax now also compares the indices of min(dim) and max(dim). The split-constant cumsum test gives x the dtype of each subtest, so a fold that drops dtype now fails it.
  • You were right about amax, amin, max and min: on the CPU plugin, integer values go through float32 for them too, so amax over a tensor that holds 16,777,217 gives 16,777,216. prod and cumsum stay exact. The docs now list them.

clamp, clamp_min and clamp_max with tensor bounds all become clamp.Tensor before partitioning, and OpenVINO does not claim clamp.Tensor. So they already run on the CPU, before and after this change.

I kept the two partitioning points out of this PR, as you suggested.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

@claude

claude Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 29m 8s —— View job


PR Review: #24396. I found no blockers, only two optional simplifications and one untested assumption behind keeping bool min(dim)/max(dim) off OpenVINO.

Detailed review

Summary

The PR keeps bool min/max/amin/amax/minimum/maximum/clamp/sign on the CPU. It also carries the #24294 follow-ups: putting back a dropped first input, sizing OpenVINO-owned outputs to the compiled shape, folding cumsum on the CPU under any default device, taking the last inference_precision at load, failing the test runner, and docs. No blockers. I don't repeat the two partitioning points you deferred (how getitem is reported, and casting instead of the CPU). Python is blocked in this job, so nothing was run; every finding comes from reading the code.

Code Quality

  • backends/openvino/runtime/OpenvinoBackend.cpp:547-555, 664-697: create_owned_output reads the request's own output, takes its shape, and builds a matching tensor to set in its place. The helper and the tensor it makes are probably unnecessary.
    • Whether output i takes this branch depends only on the planned tensor's shape_dynamism() (line 536), so the same output takes it on every call and no borrowed tensor is ever set at that index.
    • After inference, line 581 already reads the output back with infer_request_get_output_tensor_by_index.
    • If these outputs skip infer_request_set_output_tensor_by_index, OpenVINO writes to its own output and grows it. The comment at 672-674 says that output already has the compiled shape.
    • That would remove create_owned_output, ExecutionHandle::owned_outputs (OpenvinoBackend.h:33), its setup in init (427-441) and its cleanup in destroy (610-615).
    • Not run. This relies on OpenVINO keeping its own output for an index that is never set. I did not check that on devices other than the CPU.
    • Fix this →
  • backends/openvino/partitioner.py:39: [n.meta.get("val") for n in node.all_input_nodes] is shorter and matches _inputs and _constant_nodes in _passes/fold_constant_cumsum_pass.py. It would also see tensors passed in kwargs or lists. None of the ten ops takes one today, so this is only a simplification. Fix this →

Testing

  • backends/openvino/partitioner.py:49-62 (low confidence: I did not check which TopK mode the OpenVINO frontend uses for min(dim)/max(dim)):
    • The PR's argument against a uint8 cast is that OpenVINO does not specify how it orders ties in the indices of min(dim) and max(dim).
    • But OpenVINO already takes integer min(dim) and max(dim), and uint8 or int8 data has just as many ties.
    • So either OpenVINO picks the first index like PyTorch in practice, and the deferred cast would work, or those ops already return wrong indices.
    • No test covers integer ties. A uint8 or int8 input with repeated values in each row would show which case holds, before the follow-up is designed.
    • Fix this →

Recommendation

Approve. Nothing blocks merging. Both Code Quality points are optional, and the tie test can go with the deferred follow-up.

· [Branch: openvino-bool-minmax](https://github.com/pytorch/executorch/tree/openvino-bool-minmax)

The docs did not say which CPUs compute integer ops in float32, and they
named the reductions only. Measured with plain OpenVINO 2026.4.1 on the
CPU device and the f32 hint: on x86, sum, max and min of int32 and int64
values go through float32 (the max of 16,777,217 and 16,777,219 gives
16,777,220), while element-wise ops stay exact. On an ARM CPU, element-wise
add, maximum and clamp go through float32, and so do reductions along any
but the last dimension, while reductions along the last one stay exact.
The paragraph now gives one sentence per CPU, in plainer English.
"On an x86 CPU with bfloat16 units that is slower" first reads as if
"that is slower" describes the CPU. Say that float32 is slower there.
"Meta, unlike CUDA, fails on every build" packed the reason into a riddle,
and "Meta" at the start of a sentence reads like the company. Say that
without the fix the meta device keeps the cumsum unfolded on every build,
because a meta tensor has no data to copy to the CPU, while CUDA would
only show it on a build without CUDA. Comment only.
_reads_only_bool now collects its inputs with node.all_input_nodes, like
the cumsum fold does, so it also sees a tensor passed by keyword. None of
the ops it guards takes one today, so nothing changes for them. The
docstring now says that OpenVINO promotes mixed tensor inputs: a scalar
bound, as in clamp(mask, min=0.5), is converted to the input's bool type,
which is why only tensors are checked.
…odel

An owned output buffer took its starting shape from the infer request's
output at that moment. On a delegate handle shared by several calls, as
in programs from ExecuTorch 0.7 and earlier, an earlier call may have
bound its own buffer there, so a small dynamic output could start with a
large static output's shape and reserve that much memory again.

The shape now comes from the compiled model's output port, empty along
each dynamic dimension, which no call can change. It no longer depends on
what a plugin's request holds before or between inferences.

The shared-handle test also runs a static call before a dynamic one. If
dynamic outputs were left unbound instead of owned, OpenVINO would use
the earlier call's buffer there, and this test fails.
No test reached the two checks _match_partition_inputs gained: put the
first input back only when it is unused, and fail when the counts still
differ. A direct test now hands it a converted model without its first
input, once for a graph that never reads that input and once for one
that does. The comment above the count check also no longer says it
catches swapped inputs: a wrong count makes every call fail, and the
check reports that at lowering instead.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 19:18
@shoumikhin

Copy link
Copy Markdown
Contributor Author

Thanks. On your three points:

  • _reads_only_bool now reads node.all_input_nodes. Partitions are unchanged on every graph I probed.
  • create_owned_output stays. On a delegate shared by several calls, an earlier call's static output is still set at that index, so leaving the output unset makes OpenVINO use that buffer for the next result. The shared-handle test now runs a static call first and fails with that change. The owned buffer's starting shape now also comes from the compiled model rather than the request, so it no longer copies that earlier call's size.
  • Ties: the second case holds. On rows of 64 values, OpenVINO's min(dim) and max(dim) return a later index than PyTorch in many rows, for uint8, int32 and float32. That is older than this change, and a uint8 cast for bool would hit it too. A tie test for other dtypes belongs with a fix for that.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@claude

claude Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 37m 23s —— View job


PR Review: #24396. I found no blockers, only two minor points. First, if OpenVINO ever compiles an output with a dynamic rank, that output now fails on every call where it used to run. Second, the new direct test of _match_partition_inputs is in the wrong class and cannot see the parameter order.

Detailed review

Summary

The PR keeps min/max/amin/amax/minimum/maximum/clamp/sign on the CPU when every tensor they read is bool. It also carries the #24294 follow-ups. Nothing blocks merging, and both findings below are minor. Python is blocked in this job, so nothing was run; every finding comes from reading the code.

Backward Compatibility

  • backends/openvino/runtime/OpenvinoBackend.cpp:695-700 (low confidence that this is reachable: I found no partition that OpenVINO compiles with a dynamic-rank output, and OpenVINO's shape checks are from memory, since its source is not available here): create_owned_output returns NotSupported when the compiled output has a dynamic rank, so every call of such a delegate fails.
    • The removed create_ov_tensor(..., own_memory=true) path gave that output an empty buffer with the ExecuTorch tensor's rank. OpenVINO accepts any buffer shape for an output whose rank is dynamic, so a program with such an output ran before this change.
    • That rank is a safe place to start. copy_output already requires OpenVINO's result to have it, because resize_tensor cannot change a tensor's rank.
    • Fix: when compiled.rank.min != compiled.rank.max, start the buffer at output_tensor.dim() zeros and don't read compiled.dims. To do this, pass the tensor or its rank into create_owned_output.
    • Fix this →

Code Quality

  • backends/openvino/tests/ops/test_reductions.py:116-142: test_only_an_unused_first_input_is_put_back calls preprocess._match_partition_inputs directly and needs no device. Yet it sits in TestIntegerReductions(BaseOpenvinoOpTest). This file keeps such tests in their own unittest.TestCase, like TestFoldConstantCumsumPass (line 195).
    • Both inputs are f32 [2], so the count check at line 134 is the only check that can fail. The test still passes with model.add_parameters([*parameters, first]). Only the end-to-end test at line 101 catches that version, and only because its mask input is int32.
    • Fix: move the test to its own class. Give the first input a dtype or shape that differs from the second, and check parameter 0.
    • Fix this →

Recommendation

Approve. Nothing blocks merging. Both fixes are small. The first one matters only if OpenVINO can compile a partition with a dynamic-rank output.

· [Branch: openvino-bool-minmax](https://github.com/pytorch/executorch/tree/openvino-bool-minmax)

The direct test of _match_partition_inputs gave both inputs the same
shape and dtype and checked only how many parameters came back, so a
version that appended the restored input, or built it from the second
input, still passed. The runtime binds inputs by position, so either
mistake would feed one input to the other's parameter. The test now
gives the first input its own shape and dtype and checks both
parameters in order. It needs no device, so it moves out of the op test
class into its own unittest.TestCase.
An output that OpenVINO compiled with a dynamic rank cannot get a
buffer, because an ExecuTorch tensor keeps its rank. No partition tried
so far compiles to one, but if one does, the error now says so and
names the fix: export with static sizes for the inputs that output
depends on.
…ties

The CPU precision notes listed where OpenVINO's results differ from
eager PyTorch, but not the indices min(dim) and max(dim) return on
ties: PyTorch returns the first one, and OpenVINO's CPU plugin can
return a later one, for uint8 and other dtypes. Both pages now say so.
"The sum 16,777,216 + 1" read like element-wise add, which x86 computes
exactly; the example is about the sum reduction. It now says sum over a
tensor that holds both values.
Reading masked values after writing them, as in

    x[mask] = x[mask] * 2
    return x + x[mask].sum()

failed to lower with "Dynamic rank for aten::index input is not
supported". OpenVINO converts index_put to a placeholder whose rank it
takes from the decoder, and its FX decoder reads ranks only from each
node's tensor_meta. ExecuTorch edge graphs leave tensor_meta empty and
keep the shape in val. Preprocess now fills tensor_meta from val before
conversion, the way other ExecuTorch passes do. OpenVINO releases that
do not delegate index_put, such as 2025.1.0, skip the new test.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 21:10
@shoumikhin shoumikhin changed the title OpenVINO: fix bool min/max lowering and follow-ups to #24294 OpenVINO: fix bool min/max and masked read lowering, and follow-ups to #24294 Nov 1, 2026
@shoumikhin

Copy link
Copy Markdown
Contributor Author

I kept the failure because seven graphs built to get a dynamic-rank output all got a fixed rank, but the error now says to export with static sizes for the inputs that output depends on.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@claude

claude Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

PR Review: #24396

  • Read the PR context and the review checklist
  • Get the diff against origin/main (the shallow checkout has no merge base, so I used a two-dot diff limited to the 14 changed files, which matches the PR's +435/-75 for every file)
  • Run three independent reviews
  • Merge duplicate findings
  • Fact-check the findings
  • Post the review

View job run

This branch has not been deployed

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. release notes: openvino OpenVino backend related issues, features, bugs etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants