Skip to content

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

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

shoumikhin wants to merge 34 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, 6 Unrelated Failures, 2 Unclassified Failures

As of commit b7623fe 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).
@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

Claude finished @shoumikhin's task in 25m 31s —— View job


PR Review: #24396. I found no blockers and one minor docs point: the new tie note names uint8 but not float32, and the new notes on OpenVINO's CPU behavior don't say which release they describe.

Detailed review

Summary

The PR keeps bool min/max/amin/amax/minimum/maximum/clamp/sign on the CPU, fills tensor_meta so the masked read lowers, and carries the #24294 follow-ups. Nothing blocks merging, and the one finding below is minor.

  • I didn't repeat points you already deferred or decided in earlier rounds: how getitem is reported, casting bool inputs inside OpenVINO, and failing on an output compiled with a dynamic rank.
  • Nothing was run. The finding comes from reading the docs and the PR discussion.

API Design

  • backends/openvino/README.md:104-106 and docs/source/build-run-openvino.md:158-160: the tie note names uint8 but not float32.
    • "This happens with uint8 and other dtypes" singles out uint8 because of the bool-to-uint8 cast discussed on this PR. Readers of the docs never see that discussion.
    • The note comes right after the paragraph on integer precision. A reader with a float32 model, the common case, can take it as an integer-only quirk and expect the indices of min(dim) and max(dim) to match PyTorch. Your measurements found the later index for float32 as well as for uint8 and int32.
    • Fix: name float32, for example "This happens with float32 as well as integer dtypes such as int32 and uint8."
    • The same new text (README 95-106, docs 149-160) also doesn't say which OpenVINO release it describes.
      • The docs support openvino>=2025.1.0,<2027.0.0 (build-run-openvino.md:64). The PR description says the ARM list was measured with 2026.4.1.
      • _passes/fold_constant_cumsum_pass.py:111-112 already ties its bug to "2025.1 to 2026.4".
      • Naming the release would let readers tell when these plugin details have changed.
    • Fix this →

Recommendation

Approve. Nothing blocks merging. The only finding is a small docs edit in the two mirrored files.

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

The lint job runs mypy without OpenVINO installed, so the op_support
import that the new index_put test needs failed with import-not-found.
The file now turns that check off, as test_reductions.py does for the
same import.
The new notes on the CPU plugin did not say which OpenVINO release they
describe, and the tie note named uint8 but not float32, so a reader with
a float32 model could take it for an integer-only quirk. Both notes now
name OpenVINO 2026.4.1, the release they were measured with, and the tie
note names float32 as well as int32 and uint8.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 22:23
@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 42m 52s —— View job


PR Review: #24396. I found no blockers and two minor points, neither a regression. First, preprocess still fails to lower when OpenVINO drops a first input that only ones_like reads. Second, an owned output can start smaller than the minimum size OpenVINO compiled for it.

Detailed review

Summary

The PR keeps bool min/max/amin/amax/minimum/maximum/clamp/sign on ExecuTorch's CPU kernels, fills tensor_meta so the masked read lowers, and carries the #24294 follow-ups. Nothing blocks merging. Both findings below are minor, and the models they describe already failed before this PR.

  • I didn't repeat points you deferred or decided in earlier rounds: how getitem is reported, casting bool inputs inside OpenVINO, and failing on an output with a dynamic rank.
  • Nothing was run. Python is blocked in this job and the OpenVINO source isn't available, so both findings come from reading the code plus OpenVINO behavior I remember but could not check.

Code Quality

  • backends/openvino/preprocess.py:50 (medium confidence: _reads_only_shape says OpenVINO folds such a full_like, but I am going from memory that it folds before the converter drops an unused parameter 0): the first input is put back only when its placeholder has no FX users. The converter, though, drops parameter 0 when nothing reads it in the converted model. An earlier review round suggested this FX check, and it turns out to be narrower than the converter's rule.
    • The two rules disagree when the first input feeds only a full_like with a static shape. _reads_only_shape (_passes/fold_constant_cumsum_pass.py:17-21) says OpenVINO folds that full_like into a constant. That leaves the input with no reader, so the converter drops it as self.
    • Trigger: forward(x, y): return torch.ones_like(x) + y with static shapes, which is the test at tests/ops/test_reductions.py:101 without the cumsums. FX still shows full_like reading x, so lowering fails with "expected 2 parameters in the converted model, got 1".
    • Before this PR such a model failed on every call. It is the same OpenVINO behavior the PR fixes for the cumsum fold, reached another way.
    • Fix: put the input back when all(_reads_only_shape(u) for u in placeholders[0].users), which is also true when there are no users. Keep the count check, so that TestMatchPartitionInputs still covers x + y, and add the test above.
    • Fix this →
  • backends/openvino/runtime/OpenvinoBackend.cpp:712 (low confidence: I recall that the CPU plugin's set_tensor refuses a buffer whose shape is not compatible with the output's partial shape, but I could not read it, and no test here has such an output): a dynamic dimension that OpenVINO compiled with a lower bound above 0 starts at 0, which is outside its range.
    • Preprocess passes every symbolic size to OpenVINO as -1 (preprocess.py:116). A lower bound appears when the partition adds a fixed size to a dynamic one, for example torch.cat of a fixed tensor and a dynamic one, or F.pad along a dynamic dimension.
    • Trigger: torch.cat([torch.zeros(1, 8), x]) with dim 0 of x dynamic. OpenVINO compiles dim 0 of the output with a lower bound of 1, but the owned buffer starts at [0, 8]. infer_request_set_output_tensor_by_index would then fail on every call.
    • The removed all-zero code had the same gap. Still, this is the line the PR changes so that the buffer starts from the compiled shape.
    • Fix: dims[d] = std::max<int64_t>(dim.min, 0); handles fixed, bounded and unbounded dimensions in one expression. Update the comment at 706-708 to match.
    • Fix this →

Recommendation

Approve. Nothing blocks merging. Each fix is a one-line change plus a test, and neither model ran before this PR.

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

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