-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Add python bindings to read_tensor_data #32984
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
2435dd2
dce9734
3d9f4f5
9f590a7
821a3da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
|
@@ -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); | ||
praasz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // 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
|
||
| // 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) { | ||
|
|
||
olpipi marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -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", | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. C++ API
@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"); | ||||
|
|
||||
| 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 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 { | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
|
||||||||||||||||||||||||||||||||||||||
| * @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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)) { | ||
praasz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
Comment on lines
+626
to
+632
|
||
| } // namespace ov | ||
Uh oh!
There was an error while loading. Please reload this page.