Skip to content

Fix 0-d tensors, dynamic output sizes and integer reductions, and default to float32 on CPU, in the OpenVINO backend - #24294

Merged
shoumikhin merged 21 commits into
pytorch:mainfrom
shoumikhin:openvino-precision-and-scalars
Oct 30, 2026
Merged

shoumikhin merged 21 commits into
pytorch:mainfrom
shoumikhin:openvino-precision-and-scalars

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

What is wrong today

The OpenVINO backend has these problems:

  1. A 0-d (scalar) input or output fails on every run, because OpenVINO's ov_shape_create refuses rank 0.
  2. A float32 program does not compute in float32 on many CPUs. OpenVINO picks bfloat16 (two to three significant digits) on x86 CPUs with bfloat16 hardware and float16 on ARM CPUs, and no compile spec can change it.
  3. An output whose size depends on an int input keeps its largest size. x[:n].view(n, -1) with x of shape (4, 8) and n = 3 returns (4, 8).
  4. cumsum and prod of a bool or integer tensor are not computed in int64, as PyTorch does. A cumsum over a bool mask wraps after 255, so pi0 and pi0.5 get wrong position ids and wrong actions.

What this change does

  • 0-d tensors: a rank-0 tensor keeps its empty shape, which OpenVINO accepts.

  • Precision: when the device is CPU, a program computes in float32 by default, both when it is lowered and when it is loaded. A program can ask for a faster precision:

    OpenvinoPartitioner(
        [CompileSpec("device", b"CPU"), CompileSpec("inference_precision", b"bf16")]
    )

    The value is f32, f16 or bf16, checked before conversion starts. Other devices keep OpenVINO's default. On x86 CPUs OpenVINO applies the precision when the runtime loads the program, so the runtime passes it on too.

  • Dynamic output sizes: an output whose size can change goes to a buffer that OpenVINO owns, reused by every call. It starts empty and OpenVINO grows it to the largest size a call writes, so a large upper bound costs nothing until a call needs it. After inference the backend resizes the ExecuTorch output to OpenVINO's shape and copies the data in, so one delegate can also serve several calls, as programs from ExecuTorch 0.7 and earlier do. Static outputs still use the ExecuTorch buffer directly.

  • Integer reductions: the input of cumsum and prod is cast to int64 before conversion. An integer cumsum that reads only constants is computed during lowering, because OpenVINO's CPU plugin gives it wrong results from the second inference on. This includes a cumsum over ones_like or zeros_like of an input with a static shape, which OpenVINO folds into a constant too. Each one is folded on its own, and one that cannot be folded is named in a warning.

Trade-offs:

  • float32 is slower. On a Xeon with bfloat16 hardware, a 4-layer MLP takes 2.4 ms in float32 and 1.5 ms in bfloat16. On x86 this also applies to programs exported before this change, because OpenVINO applies the precision when a program loads. To get the old speed, export again with inference_precision set to bf16.
  • OpenVINO's CPU plugin computes int64 in 32 bits, so an integer result past the int32 range wraps without an error. That was already true for int64 programs, and now also applies to cumsum and prod over smaller integer types.

Tests

Each new test fails without its fix: 0-d inputs and outputs; a 256 x 256 addmm checked to 1e-4, and the same addmm lowered with bf16 checked not to reach 1e-4, so the runtime must apply an explicit precision (both skipped where OpenVINO already computes in float32); an unknown precision refused before conversion; dynamic output sizes, on one program and on one delegate shared by two calls; bool and int32 cumsum and prod; pi0-style masks with int64 and int32 cumsums over two calls, built from a constant and from ones_like; and folding when one cumsum cannot be evaluated. Two guards pass either way: integer sum, which OpenVINO already computes in int64, and a static output after a dynamic view.

The full OpenVINO op suite passes on Linux x86_64 (a CPU with bfloat16 units) with OpenVINO 2026.4.1, through both pytest and the CI test runner. With OpenVINO 2025.1.0 it also passes, except that the prod test is skipped because that release does not delegate prod.

Copilot AI balanced review requested due to automatic review settings October 29, 2026 15:38
@pytorch-bot

pytorch-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

✅ No Failures

As of commit cc9b095 with merge base b50c571 (image):
💚 Looks good so far! There are no failures yet. 💚

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 Oct 29, 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.

@shoumikhin shoumikhin added release notes: openvino OpenVino backend related issues, features, bugs etc. and removed release notes: backends labels Oct 29, 2026
@shoumikhin
shoumikhin force-pushed the openvino-precision-and-scalars branch from ca054b6 to b9d75ec Compare October 29, 2026 18:00
Copilot AI balanced review requested due to automatic review settings October 29, 2026 18:00

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.

@shoumikhin
shoumikhin force-pushed the openvino-precision-and-scalars branch from b9d75ec to 3fb1360 Compare October 29, 2026 21:40
Copilot AI balanced review requested due to automatic review settings October 29, 2026 21:40

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.

@shoumikhin shoumikhin changed the title Fix 0-d tensors and default to float32 on CPU in the OpenVINO backend Fix 0-d tensors and dynamic output sizes, and default to float32 on CPU, in the OpenVINO backend Oct 29, 2026
@shoumikhin
shoumikhin force-pushed the openvino-precision-and-scalars branch from 3fb1360 to bc88265 Compare October 29, 2026 23:23
Copilot AI balanced review requested due to automatic review settings October 29, 2026 23:23

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.

@shoumikhin shoumikhin changed the title Fix 0-d tensors and dynamic output sizes, and default to float32 on CPU, in the OpenVINO backend Fix 0-d tensors, dynamic output sizes and integer reductions, and default to float32 on CPU, in the OpenVINO backend Oct 29, 2026
@shoumikhin
shoumikhin force-pushed the openvino-precision-and-scalars branch from bc88265 to 03e2941 Compare October 29, 2026 23:50
Copilot AI balanced review requested due to automatic review settings October 29, 2026 23:50

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.

@shoumikhin
shoumikhin force-pushed the openvino-precision-and-scalars branch from 03e2941 to 8969a7b Compare October 30, 2026 01:27
Copilot AI balanced review requested due to automatic review settings October 30, 2026 01:27

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.

@shoumikhin
shoumikhin force-pushed the openvino-precision-and-scalars branch from 8969a7b to 89f775c Compare October 30, 2026 01:42
Copilot AI balanced review requested due to automatic review settings October 30, 2026 01:42

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.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

@claude

claude Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 38m 1s —— View job


Summary: The new float32-on-CPU default slows down programs that were exported before it on x86 CPUs with bf16 units, and neither a load-time option nor the repo's own export tools can switch it off; the other findings are minor issues in the cumsum-folding pass, the output-resize loop, the tests and the docs.

Detailed review

PR Review: #24294

Summary

This PR fixes 0-d tensors, dynamic output sizes and integer reductions in the OpenVINO backend, and makes float32 the default inference precision on CPU. That default needs discussion before merge, for three reasons:

  • It changes programs that were already exported, on x86, and nothing at load time can override it.
  • The repo's own export tools can't pass the new option.
  • It also applies to programs the user made fp16 on purpose.

All other findings are minor.

Code Quality

  • When folding fails, the pass keeps the cumsums it exists to remove.
    • What happens: on RuntimeError (fold_constant_cumsum_pass.py:112-115), every constant int64 cumsum stays in the graph. The pass's own docstring says that graph gives wrong results from the second inference on, with the affected CPU plugin. Yet lowering succeeds with only a generic warning.
    • Wider than needed: _evaluate runs for all cumsums at once (:111), so one subgraph that fails stops all the others from folding too.
    • Fix: evaluate each cumsum on its own. Then either raise, or make the warning say that results can be wrong. Fix this →
  • The new passes don't reuse ExecuTorch's existing pass utilities.
    • FoldConstantCumsumPass: it adds its own detection of constants and of writes (fold_constant_cumsum_pass.py:16-53), and runs it on the unlifted module. exir/passes/constant_prop_pass.py already has:

      • is_const;
      • is_constant_buffer, which reads graph_signature.buffers_to_mutate;
      • write detection that follows aliases.

      The new _is_written only looks at the positional args of a node's direct users. It is only correct while the unlifter's copy_ is the graph's only write.

    • CastReductionInputPass: it subclasses ExportPass but never calls super().call(). DecomposeFloorDividePass in the same package does (decompose_floor_divide_pass.py:105).

  • An invalid inference_precision is only reported after the whole conversion. The check at preprocess.py:111-115 runs after the passes and after frontend.convert. Move it to the top of preprocess, or into OpenvinoPartitioner.__init__. Fix this →
  • The Precision docs are duplicated, and they restate OpenVINO internals. Fix this →
    • Duplicated: the same text appears in backends/openvino/README.md:51-84 and in docs/source/build-run-openvino.md:105-138. Keep one copy and link to it from the other file.
    • Internals: the section describes CPU-plugin behavior that ExecuTorch doesn't control: the AVX-512 F/BW/DQ/VL and AVX2-VNNI-2 requirements, and emulation. A link to OpenVINO's INFERENCE_PRECISION_HINT docs would stay accurate longer.

Infrastructure

  • The repo's OpenVINO export paths can't pass inference_precision.
    • Where: these build only CompileSpec("device", ...):

      • get_openvino_partitioner (extension/llm/export/partitioner_lib.py:68);
      • examples/openvino/aot_optimize_and_infer.py:289;
      • examples/openvino/stable_diffusion/export_lcm.py:220,267,314.

      OpenvinoConfig (extension/llm/export/config/llm_config.py:599) has no precision field.

    • Effect: after this PR, export_llm and these examples all lower to f32 on CPU. Getting the old speed back, as the docs describe, means editing code.

    • Fix: add the option to OpenvinoConfig and get_openvino_partitioner, and add a command-line flag to the examples. Fix this →

Testing

  • Several of the fixes the PR describes have no test. Fix this →
    • sum: no test sums a bool or int tensor, although sum.dim_IntList is in REDUCTION_OPS (cast_reduction_input_pass.py:12). A useful case is mask.sum(1) with more than 255 True values.
    • prod.default: a full prod is never tested; test_prod (test_reductions.py:32) only uses prod(1).
    • 0-d inputs: no test passes a 0-d tensor into the delegate. The 0-d cases of test_mean only produce 0-d outputs. In test_view, .item() consumes the 0-d torch.tensor(3) before the partition.
    • Lost case in test_view.py:43: the new case replaces .sum(0) instead of being added next to it. This removes the test of a static output computed from a dynamic intermediate, and the new post-inference loop resizes such outputs too.
  • The float32 test may not catch a regression in CI (low confidence: this depends on the CI runner's CPU). Fix this →
    • Why:
      • test_addmm_computes_in_float32 (test_addmm.py:34) can only fail where OpenVINO's CPU default isn't f32: CPUs with bf16 units, or ARM.
      • test_addmm_passes_the_precision_to_openvino only checks the compile_model call.
      • So on a runner without bf16 units, deleting the runtime's core_set_property step (OpenvinoBackend.cpp:366-380) breaks no test.
    • Suggestion: skip the test when openvino.Core().get_property("CPU", "INFERENCE_PRECISION_HINT") already reports f32. The missing coverage then shows up as a skip.

API Design

  • The f32 default ignores the program's dtype.
    • What happens: preprocess.py:106-108 and OpenvinoBackend.cpp:350-352 pick f32 from the device name alone. A program the user made fp16 on purpose therefore also computes in f32.
    • Example: examples/openvino/stable_diffusion/export_lcm.py defaults to torch.float16 (line 45) and to the CPU device (line 397).
      • On ARM it now runs in f32 instead of native f16. The README itself says f32 is about 1.8x slower there.
      • On x86 CPUs with bf16 units it no longer runs in bf16.
    • Suggestion: apply the default only when the partition's floating-point tensors are f32, or document that fp16 programs are upcast to f32.
  • Composite device names are not handled.
    • Load failure (low confidence: this is from my memory of OpenVINO's core_impl.cpp; I didn't run it): OpenvinoBackend.cpp:366-368 passes the whole device string to ov_core_set_property. OpenVINO's Core::set_property rejects names that start with HETERO:, MULTI:, AUTO: or BATCH:. So a program with device=b"HETERO:GPU,CPU" and an explicit inference_precision would lower, then fail in init.
    • Mismatch with the docs: the default only applies when device starts with CPU (preprocess.py:107, OpenvinoBackend.cpp:350).
      • AUTO and HETERO:…,CPU keep OpenVINO's default (bf16 or f16 where the CPU supports it), even when they run on the CPU.
      • The docs say "On a CPU, a program computes in float32 by default" (README.md:53).

Backward Compatibility

  • The runtime's f32 default changes programs already in use, and nothing at load time can undo it.
    • What happens: OpenvinoBackend.cpp:350-352 sets INFERENCE_PRECISION_HINT=f32 for every CPU program without an inference_precision spec, and every .pte exported before this PR is one.
      • Effect: on x86 CPUs with bf16 units, OpenVINO applies the precision at import. There, these programs get slower after a runtime upgrade and can use more memory.
      • Size of the slowdown: the PR description's 4-layer MLP takes 2.4 ms in f32 and 1.5 ms in bf16 on a Xeon.
      • Remedy: the README's only one is to export again (README.md:81-84).
    • Option (a): have OpenvinoPartitioner write the effective precision into the program as a compile spec at lowering. Then:
      • the runtime needs no default, so old programs keep their behavior;
      • comments no longer have to keep preprocess.py:106-108 and OpenvinoBackend.cpp:348-352 in sync.
    • Option (b): honor a load-time override through BackendInitContext::get_runtime_spec<const char*>("inference_precision") (runtime/backend/backend_init_context.h:107). XNNPACK (XNNPACKBackend.cpp:103), MLX and Vulkan already do this.

Performance

  • Every output is queried and resized on every call, including static ones.
    • Cost: on each call, each output gets:
      • a get_output_tensor_by_index call and a tensor_get_shape call, both of which allocate on the heap;
      • up to two resize_tensor calls, in the restore step (OpenvinoBackend.cpp:536-552) and in the loop after inference (:576-612).
    • Fix: only non-static outputs can change size, so skip both steps when output_tensor.shape_dynamism() == TensorShapeDynamism::STATIC. Fix this →
  • FoldConstantCumsumPass stores folded results of any size, and runs for every device.
    • Size:
      • Cause: a node with no inputs, such as full or arange, counts as a constant, because all(...) over an empty list is true (fold_constant_cumsum_pass.py:50).
      • Example: torch.ones(4096, 4096, dtype=torch.long).cumsum(-1) becomes a 128 MiB buffer passed to OpenVINO, in place of one full node in the graph.
      • Precedent: exir's constant_prop_pass skips aten.full for this reason (exir/passes/constant_prop_pass.py:36-38).
    • Devices: the pass also runs for GPU and NPU (preprocess.py:92). Yet the pass's docstring (fold_constant_cumsum_pass.py:90-94) says the bug is only in the CPU plugin.
    • Tracking: the docstring links no upstream issue, so there is nothing to watch for when the pass can be removed.
    • Fix: run the pass only when device.startswith("CPU"), cap the size of folded results, and link the OpenVINO issue. Fix this →

Recommendation

Needs Discussion. Approval waits on decisions about the precision default:

  • Should programs exported before this PR keep OpenVINO's default (by recording the precision at lowering), or get a runtime override?
  • Should the repo's export paths expose the option?
  • Should fp16 programs be exempt?

All other findings are minor.


  • Gather context (diff, commits, checklist, CLAUDE.md)
  • Run independent reviews (3 reviewers)
  • Consolidate findings
  • Fact-check findings
  • Post final review

Not done: nothing was built or run. The claims about OpenVINO's behavior come from reading the code, and the ones I couldn't confirm are marked low confidence.

PyTorch computes sum, cumsum and prod of a bool or integer tensor in
int64. OpenVINO's PyTorch frontend keeps a narrower type for some of
them. A cumsum over a bool mask is computed in 8 bits, so the count
wraps after 255. A cumsum or prod over int32 returns int32. When such a
result is a delegate output, the runtime rejects it at execute:

    m = torch.rand(2, 7) > 0.5
    torch.cumsum(m, 1)   # CALL_DELEGATE execute failed: 0x1

Inside a graph the wrong values flow on silently. pi0 and pi0.5
compute their position ids as cumsum(pad_masks) - 1 over more than
255 tokens, so every position from 255 on was wrong and the policies
gave wrong actions.

Cast the input of these reductions to their result dtype before the
graph goes to OpenVINO. The open pytorch#24024 proposes a similar cast for
bool sum and cumsum.

The cast exposes an OpenVINO CPU bug, seen on 2025.4.1, 2026.0.0 and
2026.4.1 but not on the nightly: with an int64 cumsum over a constant,
a loaded program gives wrong results from the second inference on.
pi0 builds its attention masks from such a cumsum, and each call's mask
kept only what earlier calls allowed. So the backend now replaces an
int64 cumsum that reads only constants with its result before
converting. Buffers the graph writes to, random ops and higher-order ops
are not treated as constants, and other constants, such as compressed
weights, are left as they are. The fold computes on the CPU, so lowering
does not need the GPU, or the pinned memory, a constant was created
with. If the CPU cannot compute such a constant, the cumsum is left to
OpenVINO as before.

The new tests run cumsum over bool and int32, prod over int32, and
pi0-style attention masks and positions over 816 tokens through one
loaded program twice, with more valid tokens the second time. Pass tests
check that mutated buffers, random draws (direct and inside cond) and
float cumsums are not folded, that cumsums over a split constant and
over CUDA and pinned constants without a GPU are, and that a folded
result does not replace a buffer.
A bad inference_precision value was only reported after the whole graph
had been converted, which takes a while for a large model. The check now
runs first. The test makes conversion fail if it starts, so it fails when
the check comes after conversion.
FoldConstantCumsumPass evaluated every constant int64 cumsum in one go.
If any of them could not be evaluated on the CPU, none were folded, and
lowering only logged a generic warning. The unfolded cumsums then give
wrong results from the second inference on with the affected OpenVINO
CPU plugin.

The pass now evaluates each cumsum separately, so one failure no longer
keeps the others from being folded, and the warning names the cumsum and
says that its results can be wrong.
Add the cases the earlier tests left out:
- sum over a bool mask with more than 255 true values in a row, and over
  int32, which must come out in int64 as in PyTorch;
- a full prod (prod.default) next to prod over one dimension;
- 0-d tensors as inputs of the delegate, not only as outputs;
- the earlier static output computed from a data dependent view, kept
  next to the new dynamic output case.

The float32 addmm test now skips on CPUs where OpenVINO computes in
float32 anyway, so a runner that cannot check the default reports a skip
instead of a pass.
The Precision section listed which x86 instruction sets give bfloat16 and
how CPUs without bfloat16 units simulate it. That is OpenVINO's behavior
and changes with its releases, so link to OpenVINO's Precision Control
page instead, and keep only what the f16 and bf16 values do on CPUs
without float16 and on ARM.

Also say:
- that the float32 default applies when the device is CPU, and also to
  programs exported in float16;
- that bf16 brings back the old speed of an older program on x86, and
  that on ARM an older program keeps float16 until it is exported again;
- that OpenVINO's CPU plugin computes int64 in 32 bits, so an integer
  result past the int32 range wraps without an error, including the int64
  result of cumsum and prod over smaller integer types.
CastReductionInputPass also cast the input of sum to int64. OpenVINO's
PyTorch frontend already does that for sum, so the extra cast changed
nothing. Only cumsum and prod need it.

test_sum, over a bool mask with more than 255 true values in a row and
over int32, passes with and without the cast, and keeps checking that a
sum comes out in int64 as in PyTorch.
To let an output come back smaller than its upper bound, the backend
recorded each output's size at the first call on a delegate handle, and
restored it before every later call. Programs from ExecuTorch 0.7 and
earlier run every call of a lowered module on one handle, each with its
own output tensors, so a second call with a larger output than the first
failed: OpenVINO cannot write more than a borrowed buffer's shape holds.

An output whose size can change now goes to a buffer that OpenVINO owns
and grows as needed, one per output, made on first use and reused by
every call. After inference, the backend resizes the ExecuTorch output to
OpenVINO's shape and copies the data in. Each call overwrites that buffer,
so nothing from one call reaches the next. Static outputs still borrow
the ExecuTorch buffer, with no copy.

The new test lowers one module, calls it from two places with different
output sizes on one shared handle, and runs the program twice.
FoldConstantCumsumPass only folded cumsums with an int64 result. OpenVINO's
CPU plugin gives the same wrong results from the second inference on for
an int32 cumsum over a constant, for example cumsum(mask, dtype=int32), so
such a model still got wrong attention masks.

The pass now folds any integer cumsum that reads only constants. Float
cumsums are still left alone. The pi0-style mask test also runs with an
int32 cumsum.
An output whose size can change goes to a buffer OpenVINO owns, kept for
the life of the delegate. That buffer was created at the output's current
size, which on the first call is its upper bound, so a large dynamic
output kept a second full-size copy around even when calls produced much
less. For example, dynamic float32 logits of shape [1, 2048, 128256] cost
about 1 GB more.

Create it empty instead, with the output's rank. OpenVINO grows it to
the size it writes, as it already did when a call needed more.
On x86 CPUs OpenVINO takes the precision when the runtime loads a
program, so lowering alone cannot show that an explicit
inference_precision works. If the runtime dropped it, a bf16 program
would quietly compute in float32 and every test would still pass.

The float32 addmm test now also lowers the same 256x256 case with bf16
and checks that the result is not within 1e-4 of eager PyTorch. It runs
where OpenVINO's own CPU default is not float32, like the float32 check.
FoldConstantCumsumPass had its own check for buffers the graph writes to.
It only looked at positional arguments of direct users, so a write
through a view, a keyword argument or inside a higher-order op would have
let it fold a buffer with its initial value. Today the only write is the
positional copy_ the unlifted module adds, so nothing broke yet.

Use exir's _nodes_written_in_place, which constant_prop_pass already uses
and which covers those cases.
CastReductionInputPass edits the graph in place and never calls
ExportPass.call(), so it does not need ExportPass. Base it on PassBase,
like FoldConstantCumsumPass.
The Precision docs spoke of "a program exported before this default"
running "with this runtime", which stops being clear once the change is
in a release. Say instead that the default applies to every program
lowered without inference_precision, including ones exported with an
earlier ExecuTorch release.
FoldConstantCumsumPass folds an integer cumsum over constants, because
OpenVINO's CPU plugin gives wrong results for one from the second
inference on. A cumsum over torch.ones_like(x) or zeros_like(x) reads the
input, so it was not folded. But with a static shape OpenVINO folds the
shape into a constant, and the cumsum then hits the same bug: masks
built from it were wrong on a second call with more valid tokens.

ones_like and zeros_like become full_like in the edge graph, which reads
only the shape and dtype of its input. The pass now counts full_like of
a static shape as a constant, and evaluates it from that shape. The
attention mask test also builds its mask with ones_like, and fails
without this change.
@shoumikhin
shoumikhin force-pushed the openvino-precision-and-scalars branch from bd59ee0 to cc9b095 Compare October 30, 2026 13:38
Copilot AI balanced review requested due to automatic review settings October 30, 2026 13:38

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.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

@claude

claude Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 33m 59s —— View job


This PR makes OpenVINO CPU programs compute in float32 by default, but the runtime also applies that default to x86 programs exported with earlier releases with no load-time opt-out, and none of the repo's OpenVINO export tools can pass inference_precision yet (Needs Discussion; everything else is minor).

Detailed review

PR Review: #24294

Summary

The PR makes OpenVINO programs for CPU devices compute in float32 unless inference_precision asks for f16 or bf16, and applies this both at lowering and at load. It also does three smaller things:

  • casts cumsum and prod inputs to the result dtype;
  • folds integer cumsums over constants, to work around an OpenVINO CPU-plugin bug;
  • supports 0-d tensors and outputs whose size changes between calls.

Two major problems from the earlier review remain open: the load-time default changes the precision of already-deployed x86 programs, and the export tools can't set the option. The rest are minor.

Backward Compatibility

  • The runtime's f32 default changes already-deployed x86 programs, and there is no load-time opt-out (still open from the earlier review). OpenvinoBackend.cpp:351-355 sets INFERENCE_PRECISION_HINT=f32 in init for any CPU* program that has no inference_precision spec. On x86, OpenVINO applies the hint at import.

    • So a .pte lowered with an earlier ExecuTorch release switches from bf16 to f32 on bf16-capable CPUs as soon as the app picks up this runtime. By the PR's own numbers that is 2.4 ms instead of 1.5 ms, and memory use can grow.
    • README.md:83-87 documents this, but its only remedy is re-exporting.
    • The rule also exists twice, in preprocess.py:115 and in C++, kept in sync only by comments.

    Two options:

    • (a) Accept a load-time override through context.get_runtime_spec<const char*>("inference_precision") (runtime/backend/backend_init_context.h:107). XNNPACK (XNNPACKBackend.cpp:106) and CoreML (coreml_backend_delegate.mm:269-271) already do this.
    • (b) Preferred: have OpenvinoPartitioner (partitioner.py:120) write inference_precision=f32 into the compile specs when lowering for a CPU, and have init apply only explicit specs. Old programs then keep OpenVINO's default, and the C++ copy of the rule goes away.

    Fix this →

API Design

  • The f32 default depends on the device string starting with CPU (OpenvinoBackend.cpp:353, preprocess.py:115).

    • So AUTO, HETERO:GPU,CPU and MULTI:…,CPU programs that run on the CPU keep OpenVINO's bf16 or f16 default. I haven't verified this for AUTO. README.md:53 only says "When the device is CPU".
    • From memory, not verified: ov::Core::set_property rejects device names that start with HETERO:, MULTI:, AUTO: or BATCH:. If that's right, an explicit inference_precision with such a device lowers fine, because compile_model accepts it as config. init then fails at OpenvinoBackend.cpp:370-381 with InvalidArgument and the misleading message "failed to set inference precision".

    Fix this →

Infrastructure

  • None of the repo's OpenVINO export paths can pass the new inference_precision option (preprocess.py:58; still open). Each builds only CompileSpec("device", …):

    • extension/llm/export/partitioner_lib.py:68, together with OpenvinoConfig (extension/llm/export/config/llm_config.py:599), which has no field for it;
    • examples/openvino/aot_optimize_and_infer.py:289;
    • examples/openvino/stable_diffusion/export_lcm.py:220,267,314;
    • examples/models/yolo26/export_and_validate.py:107.

    The worst case is export_lcm.py. It defaults to --dtype fp16 (line 404) on --device CPU (line 397), so its default export now computes in f32 (README.md:80-81). On ARM that is about 1.8x slower (README.md:68-69), and the documented remedy means editing the script. Fix this →

Testing

  • No test covers a cumsum over ones_like or full_like of a dynamic-shape input. Only the isinstance(d, int) check in _reads_only_shape (fold_constant_cumsum_pass.py:29) stops such a cumsum from being folded into a constant with the example input's shape. Nothing in test_reductions.py exports with a Dim. Fix this →

Performance

  • Every non-static output now goes through a second buffer that OpenVINO owns (OpenvinoBackend.cpp:544-557, 576-595).

    • That buffer stays at its high-water size for the life of the loaded method, and its contents are memcpy'd into the ExecuTorch output on every call.
    • This affects ordinary dynamic batch and sequence outputs (e.g. [1, S, V] logits), not only data-dependent sizes. Neither doc mentions it.
    • Binding the ExecuTorch buffer at its planned upper-bound shape would avoid the copy. The delegate can't read that bound (TensorImpl::numel_bound_ is private), so it would have to be recorded at lowering.

    At minimum, document the cost. Fix this →

Code Quality

  • The cumsum workaround has no removal trigger and depends on a private exir helper.

    • The FoldConstantCumsumPass docstring (fold_constant_cumsum_pass.py:106-110) says the pass can go "once the oldest supported OpenVINO release has the fix", but links no OpenVINO issue or PR and names no fixed release (still open).
    • Line 12 imports _nodes_written_in_place, which is private to exir/passes/constant_prop_pass.py:188. Make it public there.

    Fix this →

  • The defaulted own_memory flag gives create_ov_tensor a second contract (OpenvinoBackend.h:56-61, OpenvinoBackend.cpp:688, 708-711). With the flag set, the function ignores the tensor's sizes and data and uses only its dtype and rank. A separate create_owned_output_tensor(scalar_type, rank) would make the call at line 550 say what it does. Fix this →

  • A failed allocateList in init returns Error::Internal and logs "failed to set up outputs (status=0)" (OpenvinoBackend.cpp:433-439). The ExecutionHandle allocation failure just above it returns MemoryAllocationFailed (line 427). Fix this →

Recommendation

Needs Discussion: approval depends on two decisions from the OpenVINO maintainers.

  • Should already-exported x86 programs change precision at load? If so, which fix: a load-time override, or recording the precision at lowering?
  • Should inference_precision be exposed in the LLM and example export paths?

The other findings are minor and can be fixed in this PR.

Not done:

  • I built and ran nothing locally, because this sandbox needs approval for Python, pip and git fetch.
  • CI's test-openvino-linux job at cc9b095 ran all 28 OpenVINO op tests with none skipped, plus 1 model test. Since nothing was skipped, that runner doesn't default to f32, so test_addmm_computes_in_float32 exercised the runtime precision path there.
  • The shallow clone has no merge base, so I reviewed git diff origin/main HEAD. Its stat matches the PR's 14 files, +930/−56.
  • Statements about OpenVINO behavior that I couldn't check are marked as unverified.

  • Gather context (diff, commits, checklist, CLAUDE.md)
  • Run independent reviews (3 reviewers)
  • Consolidate findings
  • Fact-check findings
  • Post final review

@suryasidd suryasidd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Changes look good to me.

@shoumikhin
shoumikhin merged commit 7b14148 into pytorch:main Oct 30, 2026
220 checks passed
@shoumikhin

Copy link
Copy Markdown
Contributor Author

@pytorchbot cherry-pick --onto release/1.6 -c critical

1 similar comment
@shoumikhin

Copy link
Copy Markdown
Contributor Author

@pytorchbot cherry-pick --onto release/1.6 -c critical

@pytorchbot

Copy link
Copy Markdown
Collaborator

Cherry picking #24294

The cherry pick PR is at #24377 and it is recommended to link a critical cherry pick PR with an issue.

Details for Dev Infra team Raised by workflow job

shoumikhin added a commit that referenced this pull request Oct 31, 2026
…ault to float32 on CPU, in the OpenVINO backend (#24294)

## What is wrong today

The OpenVINO backend has these problems:

1. A 0-d (scalar) input or output fails on every run, because OpenVINO's
`ov_shape_create` refuses rank 0.
2. A float32 program does not compute in float32 on many CPUs. OpenVINO
picks bfloat16 (two to three significant digits) on x86 CPUs with
bfloat16 hardware and float16 on ARM CPUs, and no compile spec can
change it.
3. An output whose size depends on an int input keeps its largest size.
`x[:n].view(n, -1)` with x of shape (4, 8) and n = 3 returns (4, 8).
4. `cumsum` and `prod` of a bool or integer tensor are not computed in
int64, as PyTorch does. A cumsum over a bool mask wraps after 255, so
pi0 and pi0.5 get wrong position ids and wrong actions.

## What this change does

- **0-d tensors:** a rank-0 tensor keeps its empty shape, which OpenVINO
accepts.
- **Precision:** when the device is `CPU`, a program computes in float32
by default, both when it is lowered and when it is loaded. A program can
ask for a faster precision:

  ```python
  OpenvinoPartitioner(
[CompileSpec("device", b"CPU"), CompileSpec("inference_precision",
b"bf16")]
  )
  ```

The value is `f32`, `f16` or `bf16`, checked before conversion starts.
Other devices keep OpenVINO's default. On x86 CPUs OpenVINO applies the
precision when the runtime loads the program, so the runtime passes it
on too.
- **Dynamic output sizes:** an output whose size can change goes to a
buffer that OpenVINO owns, reused by every call. It starts empty and
OpenVINO grows it to the largest size a call writes, so a large upper
bound costs nothing until a call needs it. After inference the backend
resizes the ExecuTorch output to OpenVINO's shape and copies the data
in, so one delegate can also serve several calls, as programs from
ExecuTorch 0.7 and earlier do. Static outputs still use the ExecuTorch
buffer directly.
- **Integer reductions:** the input of `cumsum` and `prod` is cast to
int64 before conversion. An integer cumsum that reads only constants is
computed during lowering, because OpenVINO's CPU plugin gives it wrong
results from the second inference on. This includes a cumsum over
`ones_like` or `zeros_like` of an input with a static shape, which
OpenVINO folds into a constant too. Each one is folded on its own, and
one that cannot be folded is named in a warning.

Trade-offs:
- float32 is slower. On a Xeon with bfloat16 hardware, a 4-layer MLP
takes 2.4 ms in float32 and 1.5 ms in bfloat16. On x86 this also applies
to programs exported before this change, because OpenVINO applies the
precision when a program loads. To get the old speed, export again with
`inference_precision` set to `bf16`.
- OpenVINO's CPU plugin computes int64 in 32 bits, so an integer result
past the int32 range wraps without an error. That was already true for
int64 programs, and now also applies to `cumsum` and `prod` over smaller
integer types.

## Tests

Each new test fails without its fix: 0-d inputs and outputs; a 256 x 256
addmm checked to 1e-4, and the same addmm lowered with `bf16` checked
not to reach 1e-4, so the runtime must apply an explicit precision (both
skipped where OpenVINO already computes in float32); an unknown
precision refused before conversion; dynamic output sizes, on one
program and on one delegate shared by two calls; bool and int32 cumsum
and prod; pi0-style masks with int64 and int32 cumsums over two calls,
built from a constant and from `ones_like`; and folding when one cumsum
cannot be evaluated. Two guards pass either way: integer `sum`, which
OpenVINO already computes in int64, and a static output after a dynamic
view.

The full OpenVINO op suite passes on Linux x86_64 (a CPU with bfloat16
units) with OpenVINO 2026.4.1, through both pytest and the CI test
runner. With OpenVINO 2025.1.0 it also passes, except that the prod test
is skipped because that release does not delegate prod.

(cherry picked from commit 7b14148)
shoumikhin added a commit that referenced this pull request Oct 31, 2026
The pick was made on release/1.6, which does not have #24298 (the #24222
pick) yet. #24294 builds on #24222, so the pick depends on code that
release/1.6 lacks, such as _nodes_written_in_place in exir's
constant_prop_pass, and its conflict resolution brought in some of
#24222's OpenVINO changes but not others. This merges a clean pick of
7b14148 onto #24298's branch and takes the clean pick's side in the
OpenVINO files, so that, on top of #24298, the change is exactly #24294
(7b14148). A plain merge of the two would keep two copies of
_PRECISIONS in preprocess.py.
shoumikhin added a commit that referenced this pull request Nov 1, 2026
This pick is stacked on #24298 (the #24222 pick), which has since gained
follow-up commits for the Qualcomm partitioner and the TensorRT guide.
Taking them keeps this branch equal to #24298 plus #24294, so that
refreshing it from release/1.6 after #24298 merges cannot undo them. The
merge is clean, and the follow-ups touch no OpenVINO files.
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.

4 participants