Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
1 change: 1 addition & 0 deletions src/bindings/python/src/openvino/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from openvino._pyopenvino import serialize
from openvino._pyopenvino import shutdown
from openvino._pyopenvino import save_model
from openvino._pyopenvino import read_tensor_data
from openvino._pyopenvino import layout_helpers
from openvino._pyopenvino import RemoteContext
from openvino._pyopenvino import RemoteTensor
Expand Down
2 changes: 1 addition & 1 deletion src/bindings/python/src/pyopenvino/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ endif()

target_include_directories(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")

target_link_libraries(${PROJECT_NAME} PRIVATE openvino::core::dev openvino::runtime openvino::offline_transformations)
target_link_libraries(${PROJECT_NAME} PRIVATE openvino::core::dev openvino::runtime openvino::offline_transformations openvino::runtime::dev)

set_target_properties(${PROJECT_NAME} PROPERTIES
INTERPROCEDURAL_OPTIMIZATION_RELEASE ${ENABLE_LTO}
Expand Down
20 changes: 16 additions & 4 deletions src/bindings/python/src/pyopenvino/core/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include "Python.h"
#include "openvino/core/except.hpp"
#include "openvino/runtime/make_tensor.hpp"
#include "openvino/util/common_util.hpp"
#include "pyopenvino/core/remote_tensor.hpp"
#include "pyopenvino/utils/utils.hpp"
Expand Down Expand Up @@ -302,18 +303,29 @@ py::array array_from_tensor(ov::Tensor&& t, bool is_shared) {
// Get actual dtype from OpenVINO type:
auto ov_type = t.get_element_type();
auto dtype = Common::type_helpers::get_dtype(ov_type);

auto data_ptr = std::as_const(t).data();
auto is_read_only = ov::is_tensor_read_only(t);

// Return the array as a view:
if (is_shared) {
py::array result;
if (ov_type.bitwidth() < Common::values::min_bitwidth) {
return py::array(dtype, t.get_byte_size(), t.data(), py::cast(t));
result = py::array(dtype, t.get_byte_size(), data_ptr, py::cast(t));
} else {
result = py::array(dtype, t.get_shape(), t.get_strides(), data_ptr, py::cast(t));
}
if (is_read_only) {
// Mark array as read-only
result.attr("flags").attr("writeable") = false;
}
return py::array(dtype, t.get_shape(), t.get_strides(), t.data(), py::cast(t));
return result;
}
Comment on lines +307 to 323
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

This changes the Python-visible mutability semantics for any tensor detected as ViewTensor by is_tensor_read_only(). If writable view tensors exist, they will now surface as non-writeable NumPy arrays, which is a breaking behavior change for Python users. Once is_tensor_read_only() is corrected to match the intended 'const-host view' definition, this risk should be mitigated; alternatively, scope the read-only marking to tensors created by read_tensor_data (if there is a reliable way to tag/detect that provenance).

Copilot uses AI. Check for mistakes.
// Return the array as a copy:
if (ov_type.bitwidth() < Common::values::min_bitwidth) {
return py::array(dtype, t.get_byte_size(), t.data());
return py::array(dtype, t.get_byte_size(), data_ptr);
}
return py::array(dtype, t.get_shape(), t.get_strides(), t.data());
return py::array(dtype, t.get_shape(), t.get_strides(), data_ptr);
}

py::array array_from_constant_copy(ov::op::v0::Constant&& c) {
Expand Down
35 changes: 35 additions & 0 deletions src/bindings/python/src/pyopenvino/pyopenvino.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,41 @@ PYBIND11_MODULE(_pyopenvino, m) {
regclass_graph_AttributeVisitor(m);
regclass_graph_Output<ov::Node>(m, std::string(""));
regclass_Tensor(m);

m.def(
"read_tensor_data",
Copy link
Contributor

Choose a reason for hiding this comment

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

C++ API read_tensor_data is located in src/core/include/openvino/runtime/tensor.hpp
To not bloat pyopenvino.cpp, maybe it's better to move it into regclass Tensor?

void regclass_Tensor(py::module m) {

@p-wysocki

[](const py::object& path,
const ov::element::Type& element_type,
const ov::PartialShape& shape,
std::size_t offset_in_bytes,
bool mmap) {
return ov::read_tensor_data(Common::utils::to_fs_path(path), element_type, shape, offset_in_bytes, mmap);
},
py::arg("path"),
py::arg("element_type") = ov::element::u8,
py::arg("shape") = ov::PartialShape::dynamic(1),
py::arg("offset_in_bytes") = 0,
py::arg("mmap") = true,
R"(
Read a tensor content from a file. Only raw data is loaded.

:param path: path to file to read
:type path: Union[str, bytes, pathlib.Path]
:param element_type: tensor element type (default: openvino.Type.u8)
:type element_type: openvino.Type
:param shape: shape for resulting tensor. If provided shape is static, specified number of elements is read only.
One of the dimensions can be dynamic. In this case it will be determined automatically based on the
length of the file content and `offset_in_bytes` (default: openvino.PartialShape.dynamic(1)).
:type shape: openvino.PartialShape
:param offset_in_bytes: read file starting from specified offset (default: 0)
:type offset_in_bytes: int
:param mmap: use mmap so real reads are deferred until tensor data is accessed. If used, the file should not be
modified until the returned tensor is destroyed (default: True)
:type mmap: bool
:return: resulting tensor
:rtype: openvino.Tensor
)");

regclass_graph_descriptor_Tensor(m);
// https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html#making-opaque-types
py::bind_vector<ov::TensorVector>(m, "TensorVector");
Expand Down
223 changes: 223 additions & 0 deletions src/bindings/python/tests/test_runtime/test_read_tensor_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018-2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

from pathlib import Path

import numpy as np
import pytest

import openvino as ov


def _write_bytes(path: Path, data: bytes) -> None:
path.write_bytes(data)
assert path.exists()


@pytest.mark.parametrize(
("dtype", "ov_type"),
[
(np.float32, ov.Type.f32),
(np.float64, ov.Type.f64),
(np.int8, ov.Type.i8),
(np.int16, ov.Type.i16),
(np.int32, ov.Type.i32),
(np.int64, ov.Type.i64),
(np.uint8, ov.Type.u8),
(np.uint16, ov.Type.u16),
(np.uint32, ov.Type.u32),
(np.uint64, ov.Type.u64),
],
)
@pytest.mark.parametrize("mmap", [True, False])
def test_read_tensor_data_typed(tmp_path: Path, dtype: np.dtype, ov_type: ov.Type, mmap: bool) -> None:
shape = (10, 20, 3, 2)
data = np.random.randint(0, 100, size=np.prod(shape)).astype(dtype, copy=False).reshape(shape)
path = tmp_path / "tensor.bin"
data.tofile(path)

tensor = ov.read_tensor_data(path, element_type=ov_type, shape=ov.PartialShape(list(shape)), mmap=mmap)
assert tensor.get_shape() == list(shape)
assert tensor.get_element_type() == ov_type
assert np.array_equal(tensor.data, data)


@pytest.mark.parametrize("mmap", [True, False])
def test_read_tensor_data_string_tensor_throws(tmp_path: Path, mmap: bool) -> None:
path = tmp_path / "tensor.bin"
_write_bytes(path, b"abc")
with pytest.raises(RuntimeError):
ov.read_tensor_data(path, element_type=ov.Type.string, mmap=mmap)


@pytest.mark.parametrize("mmap", [True, False])
def test_read_tensor_data_with_offset(tmp_path: Path, mmap: bool) -> None:
shape = (1, 2, 3, 4)
data = np.random.uniform(0.0, 1.0, size=np.prod(shape)).astype(np.float32).reshape(shape)
dummy = np.array([0.0], dtype=np.float32)

path = tmp_path / "tensor.bin"
_write_bytes(path, dummy.tobytes() + data.tobytes())

tensor = ov.read_tensor_data(
path,
element_type=ov.Type.f32,
shape=ov.PartialShape(list(shape)),
offset_in_bytes=dummy.nbytes,
mmap=mmap,
)
assert tensor.get_shape() == list(shape)
assert np.array_equal(tensor.data, data)


@pytest.mark.parametrize("mmap", [True, False])
def test_read_tensor_data_small_file_throws(tmp_path: Path, mmap: bool) -> None:
shape = (1, 2, 3, 4)
data = np.random.uniform(0.0, 1.0, size=np.prod(shape)).astype(np.float32)
path = tmp_path / "tensor.bin"
data.tofile(path)

too_big_shape = ov.PartialShape([10, 2, 3, 4])
with pytest.raises(RuntimeError):
ov.read_tensor_data(path, element_type=ov.Type.f32, shape=too_big_shape, mmap=mmap)


@pytest.mark.parametrize("mmap", [True, False])
def test_read_tensor_data_too_big_offset_throws(tmp_path: Path, mmap: bool) -> None:
shape = (1, 2, 3, 4)
data = np.random.uniform(0.0, 1.0, size=np.prod(shape)).astype(np.float32)
path = tmp_path / "tensor.bin"
data.tofile(path)

file_size = path.stat().st_size
assert file_size == data.nbytes

with pytest.raises(RuntimeError):
ov.read_tensor_data(
path,
element_type=ov.Type.f32,
shape=ov.PartialShape(list(shape)),
offset_in_bytes=1,
mmap=mmap)

with pytest.raises(RuntimeError):
ov.read_tensor_data(
path,
element_type=ov.Type.f32,
shape=ov.PartialShape(list(shape)),
offset_in_bytes=file_size,
mmap=mmap,
)

with pytest.raises(RuntimeError):
ov.read_tensor_data(
path,
element_type=ov.Type.f32,
shape=ov.PartialShape(list(shape)),
offset_in_bytes=file_size + 1,
mmap=mmap,
)


def test_read_tensor_data_default_all_args(tmp_path: Path) -> None:
"""Test main use case: only path specified, all other args use defaults."""
data = np.arange(24, dtype=np.uint8)
path = tmp_path / "tensor.bin"
data.tofile(path)

# Main use case - only specify path
tensor = ov.read_tensor_data(path)

assert isinstance(tensor, ov.Tensor)
assert tensor.get_element_type() == ov.Type.u8 # default element type
assert tensor.get_shape() == [data.size] # dynamic shape inferred from file
assert not tensor.data.flags.writeable # read-only
assert np.array_equal(tensor.data, data)


@pytest.mark.parametrize("mmap", [True, False])
def test_read_tensor_data_dynamic_shape(tmp_path: Path, mmap: bool) -> None:
shape = (1, 2, 3, 4)
data = np.random.uniform(0.0, 1.0, size=np.prod(shape)).astype(np.float32)
path = tmp_path / "tensor.bin"
data.tofile(path)

tensor = ov.read_tensor_data(path, element_type=ov.Type.f32, shape=ov.PartialShape.dynamic(1), mmap=mmap)
assert tensor.get_shape() == [data.size]
assert np.array_equal(tensor.data, data)

# default element type is u8 and default shape is dynamic(1)
tensor_u8 = ov.read_tensor_data(path, mmap=mmap)
expected_u8 = np.fromfile(path, dtype=np.uint8)
assert tensor_u8.get_shape() == [expected_u8.size]
assert tensor_u8.get_element_type() == ov.Type.u8
assert np.array_equal(tensor_u8.data, expected_u8)


@pytest.mark.parametrize("mmap", [True, False])
def test_read_tensor_data_1_dynamic_dimension(tmp_path: Path, mmap: bool) -> None:
# Last dimension is inferred from file size
shape = [1, 2, 3, 4]
data = np.random.uniform(0.0, 1.0, size=np.prod(shape)).astype(np.float32)
path = tmp_path / "tensor.bin"
data.tofile(path)

shape_with_dynamic_last = ov.PartialShape([1, 2, 3, -1])
tensor = ov.read_tensor_data(path, element_type=ov.Type.f32, shape=shape_with_dynamic_last, mmap=mmap)
assert tensor.get_shape()[-1] == shape[-1]
assert np.array_equal(tensor.data, data.reshape(shape))


@pytest.mark.parametrize("mmap", [True, False])
def test_read_tensor_data_wrong_dynamic_shape_throws(tmp_path: Path, mmap: bool) -> None:
shape = [1, 2, 3, 4]
data = np.random.uniform(0.0, 1.0, size=np.prod(shape)).astype(np.float32)
path = tmp_path / "tensor.bin"
data.tofile(path)

wrong_shape = ov.PartialShape([1, 2, 100, -1])
with pytest.raises(RuntimeError):
ov.read_tensor_data(path, element_type=ov.Type.f32, shape=wrong_shape, mmap=mmap)


@pytest.mark.parametrize("mmap", [True, False])
def test_read_tensor_data_type_doesnt_fit_file_size(tmp_path: Path, mmap: bool) -> None:
path = tmp_path / "tensor.bin"
# 3 bytes: not divisible by sizeof(float)
_write_bytes(path, b"abc")
with pytest.raises(RuntimeError):
ov.read_tensor_data(path, element_type=ov.Type.f32, mmap=mmap)


@pytest.mark.parametrize("mmap", [True, False])
def test_read_tensor_data_null_shape_throws(tmp_path: Path, mmap: bool) -> None:
shape = [1, 2, 3, 4]
data = np.random.uniform(0.0, 1.0, size=np.prod(shape)).astype(np.float32)
path = tmp_path / "tensor.bin"
data.tofile(path)

# One null dimension (0) and one dynamic dimension
null_shape = ov.PartialShape([0, ov.Dimension.dynamic(), 3, 4])
with pytest.raises(RuntimeError):
ov.read_tensor_data(path, element_type=ov.Type.f32, shape=null_shape, mmap=mmap)


@pytest.mark.parametrize("mmap", [True, False])
def test_read_tensor_data_returns_readonly_array(tmp_path: Path, mmap: bool) -> None:
"""Test that tensors from read_tensor_data have read-only numpy arrays."""
shape = (2, 3, 4)
data = np.random.uniform(0.0, 1.0, size=np.prod(shape)).astype(np.float32).reshape(shape)
path = tmp_path / "tensor.bin"
data.tofile(path)

tensor = ov.read_tensor_data(path, element_type=ov.Type.f32, shape=ov.PartialShape(list(shape)), mmap=mmap)

# Verify the numpy array is read-only
assert not tensor.data.flags.writeable

# Verify attempting to write raises an error
with pytest.raises(ValueError, match="read-only"):
tensor.data[0, 0, 0] = 999.0
13 changes: 12 additions & 1 deletion src/inference/dev_api/openvino/runtime/make_tensor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

#pragma once

#include "openvino/runtime/allocator.hpp"
#include "openvino/runtime/common.hpp"
#include "openvino/runtime/itensor.hpp"
#include "openvino/runtime/so_ptr.hpp"
#include "openvino/runtime/allocator.hpp"

namespace ov {

Expand Down Expand Up @@ -97,4 +97,15 @@ OPENVINO_RUNTIME_API ov::SoPtr<ov::ITensor> get_tensor_impl(const ov::Tensor& te
*/
OPENVINO_RUNTIME_API size_t get_tensor_data_offset(const ov::ITensor& tensor);

/**
* @brief Checks if the tensor is read-only
*
* @details A tensor is considered read-only if it was created as a view tensor from a const pointer
* using the make_tensor() function that accepts const void* host_ptr.
*
* @param tensor OpenVINO Tensor to check
*
* @return true if the tensor is read-only, false otherwise
Comment on lines +103 to +108
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The documented definition ('view tensor from a const pointer') doesn’t match the current implementation (which checks only whether the impl is ViewTensor). Either tighten the implementation to reflect this definition, or update the documentation to describe the actual condition being tested so callers don’t rely on incorrect semantics.

Suggested change
* @details A tensor is considered read-only if it was created as a view tensor from a const pointer
* using the make_tensor() function that accepts const void* host_ptr.
*
* @param tensor OpenVINO Tensor to check
*
* @return true if the tensor is read-only, false otherwise
* @details A tensor is considered read-only when its underlying implementation is a view
* tensor that represents a non-mutable view on existing memory. This typically includes
* tensors created by the make_tensor() overload that accepts const void* host_ptr, as
* well as other APIs that may return ViewTensor-based read-only views.
*
* @note The exact condition checked by this function is whether the internal ITensor
* implementation is of type ViewTensor; callers must not rely on more specific semantics
* such as how the tensor view was originally constructed.
*
* @param tensor OpenVINO Tensor to check
*
* @return true if the tensor is read-only according to the above condition, false otherwise

Copilot uses AI. Check for mistakes.
*/
OPENVINO_RUNTIME_API bool is_tensor_read_only(const ov::Tensor& tensor);
} // namespace ov
7 changes: 7 additions & 0 deletions src/inference/src/dev/make_tensor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -623,4 +623,11 @@ size_t get_tensor_data_offset(const ov::ITensor& tensor) {
return 0;
}

bool is_tensor_read_only(const ov::Tensor& tensor) {
auto impl = get_tensor_impl(tensor);
if (std::dynamic_pointer_cast<ViewTensor>(impl._ptr)) {
return true;
}
return false;
}
Comment on lines +626 to +632
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

[BLOCKER] The is_tensor_read_only function incorrectly identifies all ViewTensor subclasses as read-only, including writable ones. The current implementation checks for ViewTensor base class, but AllocatedTensor (which owns and manages writable memory) and StridedViewTensor (writable view with strides) also inherit from ViewTensor. This will cause normal allocated tensors and writable view tensors to be incorrectly marked as read-only in Python.

The function should specifically check for ReadOnlyViewTensor and ReadOnlyStridedViewTensor, not the base ViewTensor class. The correct implementation should be:

bool is_tensor_read_only(const ov::Tensor& tensor) {
    auto impl = get_tensor_impl(tensor);
    if (std::dynamic_pointer_cast<ReadOnlyViewTensor>(impl._ptr) ||
        std::dynamic_pointer_cast<ReadOnlyStridedViewTensor>(impl._ptr)) {
        return true;
    }
    return false;
}

This is critical because it affects the Python bindings' behavior - regular allocated tensors would become read-only incorrectly, breaking existing functionality.

Copilot uses AI. Check for mistakes.
} // namespace ov
1 change: 1 addition & 0 deletions tools/benchmark_tool/openvino/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from openvino._pyopenvino import serialize
from openvino._pyopenvino import shutdown
from openvino._pyopenvino import save_model
from openvino._pyopenvino import read_tensor_data
from openvino._pyopenvino import layout_helpers
from openvino._pyopenvino import RemoteContext
from openvino._pyopenvino import RemoteTensor
Expand Down
1 change: 1 addition & 0 deletions tools/ovc/openvino/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from openvino._pyopenvino import serialize
from openvino._pyopenvino import shutdown
from openvino._pyopenvino import save_model
from openvino._pyopenvino import read_tensor_data
from openvino._pyopenvino import layout_helpers
from openvino._pyopenvino import RemoteContext
from openvino._pyopenvino import RemoteTensor
Expand Down
Loading