Skip to content

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

Open
shoumikhin wants to merge 15 commits into
pytorch:mainfrom
shoumikhin:openvino-precision-and-scalars
Open

shoumikhin wants to merge 15 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.

  • Dynamic output sizes: an output whose size can change goes to a buffer that OpenVINO owns and grows, reused by every call. 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. 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 (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; 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.

❌ 1 New Failure

As of commit bd59ee0 with merge base 7586c16 (image):

NEW FAILURE - The following job has failed:

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.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@suryasidd @daniil-lyakhov @anzr299 could you take a look? This changes the OpenVINO backend.

The OpenVINO backend built every input and output tensor's shape with
ov_shape_create, which refuses rank 0. So a delegate with a 0-d input or
output, such as (x * 2).sum(), failed every run with an Internal error
and nothing in the log.

Keep the empty shape for a 0-d tensor; ov_tensor_create_from_host_ptr
takes it as a scalar. Also log both error returns of create_ov_tensor.

The 0-d cases of the existing OpenVINO mean tests failed this way and
now pass: the op tests go from 25 failures to 21, with no new failure.
…ption

OpenVINO picks the inference precision when it imports a compiled
model, and on CPUs with bfloat16 units it picks bfloat16. The backend
set no precision, so a float32 program ran in bfloat16 there. A small
float32 MLP came out with a relative error of 4e-3 against eager
PyTorch instead of 1e-6, and no compile spec could change it.

On a CPU the backend now asks for float32 by default. A program can opt
into the faster, less precise mode when it is lowered:

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

The value is OpenVINO's INFERENCE_PRECISION_HINT, and it works for any
device. Other devices, such as GPUs, keep OpenVINO's default unless the
option is set, since their compiled kernels pick their own precision. A
value OpenVINO rejects fails the load.

The trade-off: on CPUs with bfloat16 units the default is slower than
before, for float16 programs too, which now compute in float32. On a
Xeon with bfloat16 units, 8 cores, a 4 x Linear(1024) MLP at batch 64
takes 2.4 ms per call in float32 and 1.5 ms in bfloat16. A program that
wants the old speed sets inference_precision to bf16.

The OpenVINO README describes the option. test_addmm.py gains a 256 x
256 case checked to 1e-4, which fails on that CPU without this change,
and a case where an unknown precision fails the load. On that CPU, 21
existing OpenVINO op tests failed their 1e-3 tolerance before this
change; all of them pass now.
On ARM CPUs OpenVINO fixes the inference precision when it compiles the
model, which happens during lowering. preprocess called compile_model
without a config, so the model was compiled in float16 there, while the
runtime later asked for float32 on import. The compiled model reported
float32 and still computed in float16: test_addmm_computes_in_float32
missed by 2e-3 on a Linux aarch64 board.

preprocess now passes INFERENCE_PRECISION_HINT to compile_model, with the
same rule as the runtime: the inference_precision compile spec, or float32
on a CPU. On x86, where OpenVINO picks the precision on import, nothing
changes. On ARM CPUs a program now computes in float32 by default too,
about 1.8x slower than float16 on an Orin Nano; it can opt back in with
inference_precision set to f16.
OpenVINO does not check INFERENCE_PRECISION_HINT the same way on every
release this backend supports. 2025.1.0 accepts a value such as f7 and
runs the model. 2026.4.1 accepts it in set_property and only fails the
import, so the load reports "failed to import model" without naming the
precision.

Check the value against f32, f16 and bf16 ourselves: at lowering, so a
typo fails at export with a ValueError that names it, and at load, so a
program exported with a bad value fails with InvalidArgument and a log
line that names it. The test now asserts the lowering error, which no
OpenVINO release produces without this check.
The load-time check has no test of its own: lowering now rejects such a
value before it can reach the runtime.
Only CPUs compute in float32 by default. OpenVINO's GPU default is
float16, and at 256 x 256 a float16 result is far outside the test's
1e-4 tolerance, so a documented --device GPU run of the op suite would
fail it. The test now skips on other devices.
… the docs

The README's Precision section described only x86. On ARM CPUs OpenVINO
would compute in float16, and the float32 default now applies there too,
so the section says how to opt back into float16. It also says that
bfloat16 keeps two to three significant digits, not about three, what a
CPU without float16 or bfloat16 support does with each value, that other
values fail at lowering, and the snippet imports what it uses.

docs/source/build-run-openvino.md mirrors this README and gets the same
section.
An output whose size depends on an int input, or on a dynamic input
size, is planned at its upper bound. The backend handed OpenVINO that
buffer and never read back the shape OpenVINO actually produced, so the
output kept its upper-bound shape. For example, with x of shape (4, 8)
and n = 3:

    x[:n].view(n, -1)   # eager: (3, 8); OpenVINO delegate: (4, 8), one extra row

After inference, the backend now reads each output's shape from
OpenVINO and resizes the ExecuTorch output to it. The data is already
laid out for that shape at the start of the buffer. A 0-d output is
skipped: it has no size to change, and newer OpenVINO releases cannot
return a rank-0 shape from their C API.

Once an output is smaller, the next call would hand OpenVINO the smaller
buffer, and OpenVINO treats a buffer's shape as its capacity, so a larger
result would fail. The backend records each output's size at the first
call, which is its upper bound, and restores it before every later call.
The sizes live in a small list from the runtime allocator, 16 int64
values per output.

test_view_data_dependent_size returned x[:n].view(n, -1).sum(0), which
hid the extra row. It now returns the view itself and runs n = 3 and
then n = 4 on the same loaded method, through a new more_inputs option
of the shared test helper. It fails without the resize, and also
without the restore.
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.
@shoumikhin
shoumikhin force-pushed the openvino-precision-and-scalars branch from 89f775c to bd59ee0 Compare October 30, 2026 05:05
Copilot AI balanced review requested due to automatic review settings October 30, 2026 05:05

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 29m 47s —— View job


Summary: Several points from the last review are fixed, but two remain: the float32 default still makes already-exported programs compute in float32 on x86, with no load-time way back, and the repo's export tools still can't pass inference_precision. The other findings are minor.

Detailed review

PR Review: #24294

Summary

This PR fixes 0-d tensors, dynamic output sizes and integer cumsum/prod in the OpenVINO backend, and makes float32 the default precision on CPU. Several findings from the last review are fixed; the ones still open are marked (still open). The two that need a decision are the float32 default for programs already exported and the export paths that can't pass the new option. Everything else is minor.

Backward Compatibility

  • On x86, programs that are already deployed switch to float32, and nothing at load time can switch them back. (still open)
    • What happens: init() sets INFERENCE_PRECISION_HINT=f32 for every CPU program without an inference_precision spec (OpenvinoBackend.cpp:353-355). Every .pte exported before this PR is one.
    • Why the docs don't cover it: the README documents the slowdown (README.md:83-86), but its only remedy is to export again. An app that ships .pte files it can't regenerate has no way back.
    • The rule exists twice: the default and the allowed values are in preprocess.py:34,115 and again in OpenvinoBackend.cpp:353-360. Only comments keep the two in sync.
    • Smallest fix: read a load-time override with context.get_runtime_spec<const char*>("inference_precision"), as XNNPACK does (XNNPACKBackend.cpp:103). Fix this →
    • Alternative: have OpenvinoPartitioner write the effective precision into the program as a compile spec. The runtime then needs no default, and old programs keep OpenVINO's default. The catch: callers of to_backend would have to pass the spec themselves.

API Design

  • A composite device with an explicit precision probably fails at load. (still open; low confidence: this relies on my memory of OpenVINO's CoreImpl::set_property, which I couldn't check here.)
    • What happens: init() passes the whole device string to ov_core_set_property (OpenvinoBackend.cpp:370-371). OpenVINO rejects set_property for names that start with HETERO:, MULTI:, AUTO: or BATCH:.
    • Why lowering doesn't catch it: preprocess passes the hint through compile_model instead (preprocess.py:116-117), so lowering never takes this path.
    • Trigger: device=b"HETERO:GPU,CPU" with inference_precision=b"f16" returns InvalidArgument from init.
    • Fix: refuse the combination next to the _PRECISIONS check (preprocess.py:62), so it fails at lowering. The docs already promise that for bad values. Fix this →

Infrastructure

  • The repo's own OpenVINO export paths can't pass inference_precision. (still open)
    • Where: these build only CompileSpec("device", ...):
      • get_openvino_partitioner (extension/llm/export/partitioner_lib.py:68); its OpenvinoConfig (extension/llm/export/config/llm_config.py:599) has no precision field;
      • 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.
    • Effect: export_llm and these examples now always lower CPU programs to float32. Following the documented remedy (lower with bf16 or f16) means editing code.
    • Fix: add the option to OpenvinoConfig and get_openvino_partitioner, and a flag to the example scripts. Fix this →

Testing

  • No test checks that the runtime applies an explicit inference_precision.
    • Why: test_addmm_passes_the_precision_to_openvino (test_addmm.py:46-62) checks only the compile_model config at lowering, and its 0.05 tolerance also passes in float32. Its own comment (:49) says that on x86 the runtime alone decides.
    • Trigger: if init() stopped reading the spec, bf16 programs would quietly run in float32 and every test would still pass. bf16 is the documented way back to the old speed.
    • Fix: where test_addmm_computes_in_float32 runs, i.e. where OpenVINO's default isn't f32, also lower the 256×256 case with bf16 and assert that it is not within 1e-4. Fix this →

Performance

  • Each output with a dynamic size keeps a second buffer at its upper bound for the delegate's whole life.
    • What happens: on the first call, the OpenVINO-owned tensor is created at the ExecuTorch output's current sizes (OpenvinoBackend.cpp:549-554, through tensor.sizes() at :672), which are the planned upper bound. It is kept next to ExecuTorch's planned buffer until destroy(), and every call copies out of it (:576-595).
    • The description doesn't match: it says the buffer "grows", but it starts at the maximum.
    • Example: dynamic full logits [1, 2048, 128256] in f32 cost about 1 GB extra.
    • Fix: start the owned tensor at zero size, so OpenVINO grows it to what it produces; the code already relies on that growth. Also mention the copy in the README. I haven't checked how a zero-size start interacts with bounded dynamic dimensions. Fix this →
  • FoldConstantCumsumPass has no size limit, runs for every device, and links no upstream issue. (still open)
    • Size: _constant_nodes counts a node with no inputs, such as full or arange, as constant (fold_constant_cumsum_pass.py:50). So torch.ones(4096, 4096, dtype=torch.long).cumsum(-1) becomes a 128 MiB int64 constant in the blob. exir's constant_prop_pass skips aten.full for this reason (exir/passes/constant_prop_pass.py:36-38).
    • Device: the pass also runs for GPU and NPU (preprocess.py:102), but its docstring names only the CPU plugin.
    • Tracking: the docstring says the pass can go once OpenVINO is fixed, but links no issue to watch.
    • Fix: run the pass only when device.startswith("CPU"), cap the folded size, and link the OpenVINO issue. Fix this →

Code Quality

  • FoldConstantCumsumPass re-implements write detection that exir already has, with fewer cases. (still open)

    • The new code: _is_written (fold_constant_cumsum_pass.py:16-25) pairs schema arguments only with the positional args of direct users.
    • What exists: _nodes_written_in_place(graph_module) (exir/passes/constant_prop_pass.py:188) takes a GraphModule, so it works on the unlifted module too. It also handles kwargs, writes through views, and higher-order ops.
    • Risk: the only write today is the positional copy_ that unlifting adds, so nothing breaks yet. A write through a view or a keyword argument would fold a buffer with its initial value.
    • Fix: reuse it, making it public if needed. Fix this →
  • CastReductionInputPass subclasses ExportPass but never calls super().call() (cast_reduction_input_pass.py:18,27). Its sibling DecomposeFloorDividePass does (decompose_floor_divide_pass.py:105). Subclass PassBase like the other new pass, or retrace like the sibling. (still open) Fix this →

  • The fold may miss a cumsum whose input becomes constant only inside OpenVINO. (low confidence: this depends on OpenVINO's constant folding.)

    • Trigger: torch.ones_like(x, dtype=torch.long).cumsum(-1), where x has a static shape.
    • Why it's missed: at graph level the cumsum reads a user input, so it is neither folded nor named in a warning. Inside OpenVINO, ShapeOf(x) is folded, so the CumSum does see a constant.
    • Why it matters: if the plugin bug hits any constant input, this pattern still gives wrong results from the second call.
    • Fix: a two-call more_inputs test would settle it. Fix this →
  • The Precision docs are duplicated, and they don't name a release. (the duplication is still open)

    • Duplicated: the same 44 lines are in backends/openvino/README.md:51-93 and in docs/source/build-run-openvino.md:105-147. Keep one copy and link to it from the other file.
    • No release: "a program exported before this default ... with this runtime" (README.md:84-85) will be unclear after the next release. Name the ExecuTorch release instead.

    Fix this →

Recommendation

Needs Discussion. Approval waits on two decisions from the OpenVINO maintainers:

  • Should programs that are already exported switch to float32 on x86, with no load-time override?
  • Should the repo's export paths expose inference_precision?

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. pip show openvino and python3 need approval in this job, so I couldn't check OpenVINO's behavior. The claims that depend on it are marked low confidence.

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