Repository navigation
OpenVINO: fix bool min/max and masked read lowering, and follow-ups to #24294 - #24396
Conversation
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'".
🔗 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. ❌ You can merge normally! (6 Unrelated Failures), 2 Unclassified FailuresAs of commit b7623fe with merge base 05092c1 ( 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. |
|
@claude /pr-review |
|
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 reviewSummaryThe PR keeps bool Code Quality
Infrastructure
Testing
API Design
Performance
RecommendationApprove. 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. |
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).
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.
|
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. |
|
@claude /pr-review |
|
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 reviewSummaryThe PR keeps bool
API Design
RecommendationApprove. Nothing blocks merging. The only finding is a small docs edit in the two mirrored files. |
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.
|
@claude /pr-review |
|
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 Detailed reviewSummaryThe PR keeps bool
Code Quality
RecommendationApprove. Nothing blocks merging. Each fix is a one-line change plus a test, and neither model ran before this PR. |
|
@pytorchbot cherry-pick --onto release/1.6 -c critical |
Cherry picking #24396Command Details for Dev Infra teamRaised by workflow job |
|
@pytorchbot cherry-pick --onto cherry-pick-24294-by-pytorch_bot_bot_ -c critical |
Cherry picking #24396The cherry pick PR is at #24446 and it is recommended to link a critical cherry pick PR with an issue. Details for Dev Infra teamRaised by workflow job |
…ductions (#24492) Supersedes #24377 with a clean release pick from #24294. Fixes scalar tensors, float32 CPU defaults, dynamic output buffers and integer reductions. Includes the related #24396 runtime, folded-cumsum, parameter-count and CI runner follow-ups previously spread across #24377 and #24446. This pick contains the complete core OpenVINO fix and its required follow-ups. Bool-op handling and masked-read metadata are isolated in the next clean pick. The source float32 default can cost speed on CPUs with bfloat16 hardware; inference_precision=bf16 remains available. ## Scope and landing order Land after #24491 in the verified landing order. The OpenVINO pair follows the CUDA picks; these backend groups can otherwise be reviewed separately. This branch starts directly at release/1.6 commit `dfb78b9ecd933a5fee33ebc94f69ed4b71cbb1e7`. Its GitHub diff contains 16 files for this pick; it does not carry duplicate #24298 history or the excluded unmerged QNN work. Dependencies are documented here rather than embedded as other cherry-pick commits. ## Verification All changed Python files compile and pass Ruff F821 checks. All seven replacements merge cleanly in the documented sequence, including simulated squash merges. No QNN files are changed. The combined OpenVINO production code matches the previously reviewed final #24446 code; the bool test class is preserved unchanged. Native CUDA, OpenVINO and portable C++ suites were not run locally on this macOS host; PR CI remains required before landing. Prepared with OpenAI Codex.
Supersedes #24446 with a clean release pick from #24396. Keeps unsupported bool-only min/max/clamp/sign operations on the CPU and populates tensor metadata for masked-read lowering. Includes their regression tests. The bool test class is moved unchanged to test_bool_min_max.py so this PR can be based directly on release/1.6 without carrying the core OpenVINO commit. ## Scope and landing order Land after #24492 in the verified landing order. This branch starts directly at release/1.6 commit `dfb78b9ecd933a5fee33ebc94f69ed4b71cbb1e7`. Its GitHub diff contains 4 files for this pick; it does not carry duplicate #24298 history or the excluded unmerged QNN work. Dependencies are documented here rather than embedded as other cherry-pick commits. ## Verification All changed Python files compile and pass Ruff F821 checks. All seven replacements merge cleanly in the documented sequence, including simulated squash merges. No QNN files are changed. The combined OpenVINO production code matches the previously reviewed final #24446 code; the bool test class is preserved unchanged. Native CUDA, OpenVINO and portable C++ suites were not run locally on this macOS host; PR CI remains required before landing. Prepared with OpenAI Codex.
What is wrong today
The OpenVINO partitioner gives
min,max,amin,amax,minimum,maximum,clampandsignto OpenVINO even when every tensor they read is bool. OpenVINO cannot convert these for bool:torch.min(x > 0, dim=1)andMultivariateNormal's argument check (PyTorch 2.15) hit this. Reading masked values after writing them fails too: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 wrongmin(dim)andmax(dim)indices on ties, as the docs now note.For the masked read, preprocess fills each node's
tensor_metafromval. OpenVINO reads ranks only fromtensor_meta, which ExecuTorch leaves empty.Follow-ups to #24294:
cumsumread, failing every call. Preprocess puts it back and checks the input count.cumsumfold works under a CUDA default device on a CPU-only PyTorch (exir's ScalarToTensorPass still fails there).inference_precisionentry now also means the default at load.What was tested
Linux x86_64, OpenVINO 2026.4.1 and 2025.1.0, CPU, unless noted:
minmodel (both releases) and the masked read (2026.4.1) failed to lower, and now match eager.tensor_metachange, all other test partitions convert to the same OpenVINO graphs.