Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
32e54f3
Fail the OpenVINO test runner when tests fail
shoumikhin Oct 30, 2026
718ebfa
Keep the first OpenVINO partition input when only a folded cumsum rea…
shoumikhin Oct 31, 2026
e2f4dbc
Give OpenVINO-owned output buffers the shape OpenVINO compiled
shoumikhin Oct 31, 2026
dec8d05
Evaluate OpenVINO full_like folds on the CPU under any default device
shoumikhin Oct 31, 2026
dc23ee3
Let the last OpenVINO inference_precision entry win at load too
shoumikhin Oct 31, 2026
abc9129
Say why the OpenVINO bf16 addmm check needs the runtime
shoumikhin Oct 31, 2026
ec69164
Keep the OpenVINO input types comment above the converter call
shoumikhin Oct 31, 2026
94475c1
Evaluate one node at a time in the OpenVINO cumsum fold
shoumikhin Oct 31, 2026
3c3a02e
Test the OpenVINO cumsum fold on an int32 cumsum
shoumikhin Oct 31, 2026
9bd7aa6
Say which x86 CPUs the OpenVINO float32 default slows down
shoumikhin Oct 31, 2026
1f34eab
Say that OpenVINO sums int64 values in float32 on CPUs
shoumikhin Oct 31, 2026
74383d7
Say which OpenVINO outputs refuse an empty buffer
shoumikhin Oct 31, 2026
8cf22fd
Say the OpenVINO cumsum fold is needed from 2025.1
shoumikhin Oct 31, 2026
1a92f87
Leave bool min, max, clamp and sign to the CPU in the OpenVINO partit…
shoumikhin Oct 31, 2026
43ab757
Return OpenVINO-owned output buffers as a Result, like create_ov_tensor
shoumikhin Nov 1, 2026
152c4f7
Test on any device that the OpenVINO runtime takes the last inference…
shoumikhin Nov 1, 2026
089968a
Run the OpenVINO model tests even when an op test fails
shoumikhin Nov 1, 2026
1c12ab7
Check that the converted OpenVINO model takes every partition input
shoumikhin Nov 1, 2026
7a67781
Compare the indices of bool min and max in the OpenVINO test
shoumikhin Nov 1, 2026
154350d
Check the dtype of a folded int32 cumsum in the OpenVINO test
shoumikhin Nov 1, 2026
076bbab
Say which OpenVINO integer reductions go through float32
shoumikhin Nov 1, 2026
3467565
Say which CPUs compute OpenVINO integer ops in float32
shoumikhin Nov 1, 2026
a4e1a36
Make a sentence in the OpenVINO precision docs read in one pass
shoumikhin Nov 1, 2026
71c32e9
Explain why the OpenVINO fold test uses the meta device
shoumikhin Nov 1, 2026
a09af5a
Read every input node in the OpenVINO bool check
shoumikhin Nov 1, 2026
f128e95
Read the starting shape of OpenVINO-owned outputs from the compiled m…
shoumikhin Nov 1, 2026
d7fc033
Test the input checks in OpenVINO preprocess directly
shoumikhin Nov 1, 2026
9f9ef96
Check the order of the inputs OpenVINO preprocess puts back
shoumikhin Nov 1, 2026
d658839
Say what to do when an OpenVINO output has a dynamic rank
shoumikhin Nov 1, 2026
21fa048
Note that OpenVINO's min(dim) and max(dim) can pick a later index on …
shoumikhin Nov 1, 2026
1e784ca
Make the OpenVINO float32 sum example read as a reduction
shoumikhin Nov 1, 2026
75d6775
Give OpenVINO the rank of every tensor in an edge graph
shoumikhin Nov 1, 2026
b964db8
Let mypy check the OpenVINO index_put test without OpenVINO
shoumikhin Nov 1, 2026
b7623fe
Name the OpenVINO release and float32 in the OpenVINO CPU notes
shoumikhin Nov 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .ci/scripts/test_openvino.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ source "$(dirname "${BASH_SOURCE[0]}")/utils.sh"

source openvino/setupvars.sh
cd backends/openvino/tests
python test_runner.py --test_type ops
python test_runner.py --test_type models
# Run the model tests even when an op test fails, and still fail the job.
status=0
python test_runner.py --test_type ops || status=1
python test_runner.py --test_type models || status=1
exit "${status}"
20 changes: 16 additions & 4 deletions backends/openvino/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,28 @@ in float32 unless you lower it with `inference_precision`.

On x86 CPUs, OpenVINO applies the precision when the runtime loads a program.
So a program lowered without `inference_precision` computes in float32, even
one exported with an ExecuTorch release that did not have this default. That
is slower and can use more memory. Export it again with `inference_precision`
set to `bf16` to get the earlier speed. On ARM CPUs, OpenVINO applies the
one exported with an ExecuTorch release that did not have this default. On an
x86 CPU with bfloat16 units, float32 is slower and can use more memory; export it
again with `inference_precision` set to `bf16` to get the earlier speed. Other
x86 CPUs already computed such a program in float32. On ARM CPUs, OpenVINO applies the
precision when the program is lowered, so a program exported with such an
earlier release keeps computing in float16 until it is exported again.

OpenVINO's CPU plugin computes 64-bit integers in 32 bits. An integer result
outside the int32 range wraps around without an error. This applies to every
int64 tensor, including the int64 result of `cumsum` and `prod` over smaller
integer types.
integer types. In OpenVINO 2026.4.1, the CPU plugin also computes some int32 and
int64 ops in float32. On x86 CPUs these are `sum`, `amax`, `amin`, `max` and
`min`. On ARM CPUs they are element-wise ops such as `add`, `maximum` and
`clamp`, and the same reductions along any dimension but the last. Once a value
or a running total is above 16,777,216 (2^24) or below -16,777,216, such a
result can be off, even inside the int32 range. For example, on x86 `sum` over a
tensor that holds 16,777,216 and 1 gives 16,777,216, and so does `amax` over one
that holds 16,777,217.

In OpenVINO 2026.4.1, the CPU plugin's `min(dim)` and `max(dim)` can return a
later index on ties than PyTorch, which returns the first one. This happens with
float32 as well as integer dtypes such as int32 and uint8.

## Directory Structure

Expand Down
21 changes: 13 additions & 8 deletions backends/openvino/_passes/fold_constant_cumsum_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@ def _constant_nodes(graph_module: torch.fx.GraphModule) -> set:
return constants


def _evaluate(graph_module: torch.fx.GraphModule, targets: list) -> dict:
"""Values of targets, computing only what they read, on the CPU."""
def _evaluate(
graph_module: torch.fx.GraphModule, target: torch.fx.Node
) -> torch.Tensor:
"""Value of target, computing only what it reads, on the CPU."""
needed = set()
stack = list(targets)
stack: list = [target]
while stack:
node = stack.pop()
if node not in needed:
Expand All @@ -82,7 +84,10 @@ def _evaluate(graph_module: torch.fx.GraphModule, targets: list) -> dict:
continue
if _reads_only_shape(node):
like = node.args[0].meta["val"]
args = (torch.empty(like.shape, dtype=like.dtype), *node.args[1:])
args = (
torch.empty(like.shape, dtype=like.dtype, device="cpu"),
*node.args[1:],
)
kwargs = dict(node.kwargs)
else:
args, kwargs = torch.fx.node.map_arg(
Expand All @@ -95,15 +100,15 @@ def _evaluate(graph_module: torch.fx.GraphModule, targets: list) -> dict:
kwargs = {**kwargs, "pin_memory": False}
values[node] = node.target(*args, **kwargs)
for i in _inputs(node):
if last_use[i] is node and i not in targets:
if last_use[i] is node:
del values[i]
return values
return values[target]


class FoldConstantCumsumPass(PassBase):
"""Replace an integer cumsum that reads only constants with its result.

With an integer cumsum over a constant, OpenVINO's CPU plugin (2025.4 to
With an integer cumsum over a constant, OpenVINO's CPU plugin (2025.1 to
2026.4) gives wrong results from the second inference on: masks built from
it keep only what earlier calls allowed. pi0 builds its attention masks
this way. The OpenVINO nightly no longer does this, so the pass can go once
Expand All @@ -130,7 +135,7 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
# the others from being folded.
for node in cumsums:
try:
value = _evaluate(graph_module, [node])[node]
value = _evaluate(graph_module, node)
except RuntimeError as e:
# Some ops have modes the CPU lacks.
logging.warning(
Expand Down
26 changes: 26 additions & 0 deletions backends/openvino/partitioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,33 @@ def __init__(self):
self.op_types = {}


def _reads_only_bool(node: torch.fx.Node) -> bool:
"""Whether OpenVINO would compute the node in bool. It promotes mixed tensor
inputs, so maximum(bool, float) is computed in float and converts fine."""
vals = [n.meta.get("val") for n in node.all_input_nodes]
tensors = [val for val in vals if isinstance(val, torch.Tensor)]
return bool(tensors) and all(t.dtype == torch.bool for t in tensors)


class OpenvinoOperatorsSupport(OperatorSupportBase):
extended_support_dict = {
"torch.ops.dim_order_ops._clone_dim_order.default": None,
"torch.ops.dim_order_ops._to_dim_order_copy.default": None,
}
# OpenVINO converts these to ReduceMin, ReduceMax, Minimum, Maximum or Sign,
# none of which take bool tensors.
bool_unsupported_ops = {
"aten.amax.default",
"aten.amin.default",
"aten.clamp.default",
"aten.max.default",
"aten.max.dim",
"aten.maximum.default",
"aten.min.default",
"aten.min.dim",
"aten.minimum.default",
"aten.sign.default",
}

def __init__(
self,
Expand Down Expand Up @@ -87,6 +109,10 @@ def is_node_supported(self, _, node: torch.fx.Node) -> bool:
if op_type == "getitem":
return True

if op_type in self.bool_unsupported_ops and _reads_only_bool(node):
print("Op not supported for bool tensors: ", "torch.ops." + str(op_type))
return False

if "torch.ops." + str(op_type) in supported_ops:
return True
else:
Expand Down
44 changes: 41 additions & 3 deletions backends/openvino/preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,42 @@
from openvino.frontend.pytorch.fx_decoder import ( # type: ignore[import-untyped]
TorchFXPythonDecoder,
)
from openvino.frontend.pytorch.utils import ( # type: ignore[import-untyped]
pt_to_ov_type_map,
)
from openvino.preprocess import PrePostProcessor # type: ignore[import-untyped]
from torch.fx.passes.shape_prop import _extract_tensor_metadata

# The values of the inference_precision compile spec. The runtime
# (OpenvinoBackend.cpp) accepts the same ones.
_PRECISIONS = ("f32", "f16", "bf16")


def _match_partition_inputs(
model, module: torch.fx.GraphModule, input_shapes: list, input_types: list
) -> None:
"""Make the converted model take the inputs the runtime passes."""
# The converter takes an unused first input for forward's self and removes
# it, but the runtime passes every partition input. The cumsum fold can
# leave the first input unused.
parameters = list(model.get_parameters())
placeholders = [n for n in module.graph.nodes if n.op == "placeholder"]
if len(parameters) == len(input_types) - 1 and not placeholders[0].users:
for parameter in parameters:
model.remove_parameter(parameter)
first = opset13.parameter(
input_shapes[0], pt_to_ov_type_map[str(input_types[0])]
)
model.add_parameters([first, *parameters])
# The runtime binds inputs by position and refuses a wrong count, so any
# other mismatch fails here instead of on every call.
if len(model.get_parameters()) != len(input_types):
raise RuntimeError(
f"OpenVINO backend: expected {len(input_types)} parameters in the "
f"converted model, got {len(model.get_parameters())}"
)


@final
class OpenvinoBackend(BackendDetails):

Expand Down Expand Up @@ -96,15 +125,24 @@ def preprocess(
f"{type(val).__name__}; only tensors and ints are supported"
)

# The converter chooses some lowerings (e.g. boolean-mask indexing) from
# the input types, so they must be known before conversion starts.
module = edge_program.module()
FoldConstantCumsumPass()(module)
# The converter reads ranks only from tensor_meta, which edge graphs
# leave empty, and some ops need one, like aten.index after
# aten.index_put.
for node in module.graph.nodes:
val = node.meta.get("val")
if isinstance(val, torch.Tensor) and not node.meta.get("tensor_meta"):
node.meta["tensor_meta"] = _extract_tensor_metadata(val)
# The converter chooses some lowerings (e.g. boolean-mask indexing) from
# the input types, so they must be known before conversion starts.
decoder = TorchFXPythonDecoder(
module, input_shapes=input_shapes, input_types=input_types
)
frontend = FrontEndManager().load_by_framework("pytorch")
ppp = PrePostProcessor(frontend.convert(frontend.load(decoder)))
model = frontend.convert(frontend.load(decoder))
_match_partition_inputs(model, module, input_shapes, input_types)
ppp = PrePostProcessor(model)
# The converter takes an int input, like a SymInt size, as a scalar, but
# the runtime passes it as an int64 tensor of shape [1].
for i in int_inputs:
Expand Down
24 changes: 24 additions & 0 deletions backends/openvino/runtime/OpenvinoApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ typedef struct ov_core ov_core_t;
typedef struct ov_compiled_model ov_compiled_model_t;
typedef struct ov_infer_request ov_infer_request_t;
typedef struct ov_tensor ov_tensor_t;
typedef struct ov_output_const_port ov_output_const_port_t;

// Value types reproduced from openvino/c/ov_shape.h and ov_common.h.
// These are stable C ABI — pinned via version constraint in pyproject.toml.
Expand All @@ -30,6 +31,17 @@ typedef struct {
int64_t* dims;
} ov_shape_t;

// Reproduced from openvino/c/ov_dimension.h and ov_partial_shape.h.
typedef struct {
int64_t min;
int64_t max;
} ov_dimension_t;

typedef struct {
ov_dimension_t rank;
ov_dimension_t* dims;
} ov_partial_shape_t;

typedef struct {
char** devices;
size_t size;
Expand Down Expand Up @@ -92,6 +104,13 @@ using ov_compiled_model_inputs_size_fn =
using ov_compiled_model_outputs_size_fn =
ov_status_e (*)(const ov_compiled_model_t*, size_t*);
using ov_compiled_model_free_fn = void (*)(ov_compiled_model_t*);
using ov_compiled_model_output_by_index_fn = ov_status_e (*)(
const ov_compiled_model_t*,
size_t,
ov_output_const_port_t**);
using ov_port_get_partial_shape_fn =
ov_status_e (*)(const ov_output_const_port_t*, ov_partial_shape_t*);
using ov_output_const_port_free_fn = void (*)(ov_output_const_port_t*);
using ov_infer_request_set_input_tensor_by_index_fn =
ov_status_e (*)(ov_infer_request_t*, size_t, const ov_tensor_t*);
using ov_infer_request_set_output_tensor_by_index_fn =
Expand All @@ -111,6 +130,7 @@ using ov_tensor_data_fn = ov_status_e (*)(const ov_tensor_t*, void**);
using ov_shape_create_fn =
ov_status_e (*)(int64_t, const int64_t*, ov_shape_t*);
using ov_shape_free_fn = ov_status_e (*)(ov_shape_t*);
using ov_partial_shape_free_fn = void (*)(ov_partial_shape_t*);

// Defined in OpenvinoBackend.cpp, where the platform's loader API is included.
struct DlCloser {
Expand All @@ -132,6 +152,9 @@ struct OpenvinoFunctions {
ov_compiled_model_inputs_size_fn compiled_model_inputs_size = nullptr;
ov_compiled_model_outputs_size_fn compiled_model_outputs_size = nullptr;
ov_compiled_model_free_fn compiled_model_free = nullptr;
ov_compiled_model_output_by_index_fn compiled_model_output_by_index = nullptr;
ov_port_get_partial_shape_fn port_get_partial_shape = nullptr;
ov_output_const_port_free_fn output_const_port_free = nullptr;
ov_infer_request_set_input_tensor_by_index_fn
infer_request_set_input_tensor_by_index = nullptr;
ov_infer_request_set_output_tensor_by_index_fn
Expand All @@ -148,6 +171,7 @@ struct OpenvinoFunctions {
ov_tensor_data_fn tensor_data = nullptr;
ov_shape_create_fn shape_create = nullptr;
ov_shape_free_fn shape_free = nullptr;
ov_partial_shape_free_fn partial_shape_free = nullptr;
};

} // namespace openvino
Expand Down
Loading