Skip to content

Commit b60f5c5

Browse files
authored
[PyAPI] improvements of memory consumption (openvinotoolkit#28878)
### Details: - Writing to stringstream caused additional copy - I've implemented custom buffer that wraps interactions with python memory without extra copies - I used ControlNet for measurement of memory consumption and and I was able to reduce memory consumption by 1.6 times for this model. ``` def test1(): core = ov.Core() model = core.read_model(xml_path) compiled_model = core.compile_model(model, device_name="CPU") user_stream = io.BytesIO() compiled_model.export_model(user_stream) ``` ![test1](https://github.com/user-attachments/assets/3d61d7b9-aa03-426e-b19f-61a4b38b972b) ``` def test2(): core = ov.Core() model = core.read_model(xml_path) compiled_model = core.compile_model(model, device_name="CPU") user_stream = compiled_model.export_model() ``` ![test2](https://github.com/user-attachments/assets/26e7fe32-47c5-4fb8-aa18-b6d268f12034) - For `read_model` and `compile_model` charts didn't show significant improvements as these changes relate to `xml` file, which is ~6 Mb ### Tickets: - [CVS-160767](https://jira.devtools.intel.com/browse/CVS-160767)
1 parent aa823d3 commit b60f5c5

File tree

6 files changed

+99
-34
lines changed

6 files changed

+99
-34
lines changed

src/bindings/python/src/openvino/_ov_api.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (C) 2018-2025 Intel Corporation
33
# SPDX-License-Identifier: Apache-2.0
44

5+
import io
56
from types import TracebackType
67
from typing import Any, Iterable, Union, Optional, Dict, Tuple, List
78
from typing import Type as TypingType
@@ -538,8 +539,8 @@ class Core(CoreBase):
538539
"""
539540
def read_model(
540541
self,
541-
model: Union[str, bytes, object],
542-
weights: Union[object, str, bytes, Tensor] = None,
542+
model: Union[str, bytes, object, io.BytesIO],
543+
weights: Union[object, str, bytes, Tensor, io.BytesIO] = None,
543544
config: Optional[dict] = None
544545
) -> Model:
545546
config = {} if config is None else config

src/bindings/python/src/pyopenvino/core/compiled_model.cpp

+19-12
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ void regclass_CompiledModel(py::module m) {
4646
cls.def(
4747
"export_model",
4848
[](ov::CompiledModel& self) {
49-
std::stringstream _stream;
50-
{
51-
py::gil_scoped_release release;
52-
self.export_model(_stream);
53-
}
54-
return py::bytes(_stream.str());
49+
py::object model_stream = py::module::import("io").attr("BytesIO")();
50+
51+
Common::utils::OutPyBuffer mb(model_stream);
52+
std::ostream _stream(&mb);
53+
54+
self.export_model(_stream);
55+
56+
_stream.flush();
57+
model_stream.attr("flush")();
58+
model_stream.attr("seek")(0); // Always rewind stream!
59+
60+
return model_stream;
5561
},
5662
R"(
5763
Exports the compiled model to bytes/output stream.
@@ -81,13 +87,14 @@ void regclass_CompiledModel(py::module m) {
8187
"`model_stream` must be an io.BytesIO object but " +
8288
(std::string)(py::repr(model_stream)) + "` provided");
8389
}
84-
std::stringstream _stream;
85-
{
86-
py::gil_scoped_release release;
87-
self.export_model(_stream);
88-
}
90+
91+
Common::utils::OutPyBuffer mb(model_stream);
92+
std::ostream _stream(&mb);
93+
94+
self.export_model(_stream);
95+
96+
_stream.flush();
8997
model_stream.attr("flush")();
90-
model_stream.attr("write")(py::bytes(_stream.str()));
9198
model_stream.attr("seek")(0); // Always rewind stream!
9299
},
93100
py::arg("model_stream"),

src/bindings/python/src/pyopenvino/core/core.cpp

+11-18
Original file line numberDiff line numberDiff line change
@@ -213,13 +213,10 @@ void regclass_Core(py::module m) {
213213
cls.def(
214214
"compile_model",
215215
[](ov::Core& self,
216-
const py::object& model_buffer,
216+
const py::object& model,
217217
const py::object& weight_buffer,
218218
const std::string& device_name,
219219
const std::map<std::string, py::object>& properties) {
220-
std::stringstream _stream;
221-
_stream << model_buffer.cast<std::string>();
222-
223220
py::buffer_info info;
224221
if (!py::isinstance<py::none>(weight_buffer)) {
225222
auto p = weight_buffer.cast<py::bytes>();
@@ -236,7 +233,7 @@ void regclass_Core(py::module m) {
236233
}
237234
auto _properties = Common::utils::properties_to_any_map(properties);
238235
py::gil_scoped_release release;
239-
return self.compile_model(_stream.str(), tensor, device_name, _properties);
236+
return self.compile_model(model.cast<std::string>(), tensor, device_name, _properties);
240237
},
241238
py::arg("model_buffer"),
242239
py::arg("weight_buffer"),
@@ -452,16 +449,14 @@ void regclass_Core(py::module m) {
452449
py::object model_path,
453450
py::object weights_path,
454451
const std::map<std::string, py::object>& config) {
455-
if (py::isinstance(model_path, pybind11::module::import("io").attr("BytesIO"))) {
456-
std::stringstream _stream;
452+
if (py::isinstance(model_path, py::module::import("io").attr("BytesIO"))) {
457453
model_path.attr("seek")(0); // Always rewind stream!
458-
_stream << model_path
459-
.attr("read")() // alternative: model_path.attr("get_value")()
460-
.cast<std::string>();
454+
py::buffer_info buffer_info = model_path.attr("getbuffer")().cast<py::buffer>().request();
455+
461456
py::buffer_info info;
462457
if (!py::isinstance<py::none>(weights_path)) {
463-
auto p = weights_path.cast<py::bytes>();
464-
info = py::buffer(p).request();
458+
py::object buffer = weights_path.attr("getbuffer")();
459+
info = py::buffer(buffer).request();
465460
}
466461
size_t bin_size = static_cast<size_t>(info.size);
467462
ov::Tensor tensor(ov::element::Type_t::u8, {bin_size});
@@ -471,7 +466,7 @@ void regclass_Core(py::module m) {
471466
std::memcpy(tensor.data(), bin, bin_size);
472467
}
473468
py::gil_scoped_release release;
474-
return self.read_model(_stream.str(), tensor);
469+
return self.read_model(std::string(static_cast<char*>(buffer_info.ptr), buffer_info.size), tensor);
475470
} else if (py::isinstance(model_path, py::module_::import("pathlib").attr("Path")) ||
476471
py::isinstance<py::str>(model_path)) {
477472
const std::string model_path_cpp{py::str(model_path)};
@@ -484,10 +479,8 @@ void regclass_Core(py::module m) {
484479
return self.read_model(model_path_cpp, weights_path_cpp, any_map);
485480
}
486481

487-
std::stringstream str;
488-
str << "Provided python object type " << py::str(model_path.get_type())
489-
<< " isn't supported as 'model' argument.";
490-
OPENVINO_THROW(str.str());
482+
throw py::type_error("Provided python object type " + (std::string)(py::str(model_path.get_type())) +
483+
" isn't supported as 'model' argument.");
491484
},
492485
py::arg("model"),
493486
py::arg("weights") = py::none(),
@@ -506,7 +499,7 @@ void regclass_Core(py::module m) {
506499
For PDPD format (*.pdmodel) weights parameter is not used.
507500
For TF format (*.pb): weights parameter is not used.
508501
For TFLite format (*.tflite) weights parameter is not used.
509-
:type weights: pathlib.Path
502+
:type weights: Union[pathlib.Path, io.BytesIO]
510503
:param config: Optional map of pairs: (property name, property value) relevant only for this read operation.
511504
:type config: dict, optional
512505
:return: A model.

src/bindings/python/src/pyopenvino/utils/utils.hpp

+48-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,52 @@ namespace py = pybind11;
3232

3333
namespace Common {
3434
namespace utils {
35+
36+
class OutPyBuffer : public std::streambuf {
37+
public:
38+
OutPyBuffer(py::object bytes_io_stream) : m_py_stream{std::move(bytes_io_stream)} {}
39+
40+
protected:
41+
std::streamsize xsputn(const char_type* s, std::streamsize n) override {
42+
return m_py_stream.attr("write")(py::bytes(s, n)).cast<std::streamsize>();
43+
}
44+
45+
int_type overflow(int_type c) override {
46+
char as_char = c;
47+
if (m_py_stream.attr("write")(py::bytes(&as_char, 1)).cast<int>() != 1) {
48+
return traits_type::eof();
49+
}
50+
return c;
51+
}
52+
53+
pos_type seekoff(off_type off, std::ios_base::seekdir dir, std::ios_base::openmode which) override {
54+
const int python_direction = [&]{
55+
switch (dir) {
56+
case std::ios_base::beg:
57+
return 0;
58+
case std::ios_base::cur:
59+
return 1;
60+
case std::ios_base::end:
61+
return 2;
62+
default:
63+
return -1;
64+
}
65+
}();
66+
if (python_direction == -1) {
67+
return pos_type(off_type(-1));
68+
}
69+
const auto abs_pos = m_py_stream.attr("seek")(off, python_direction).cast<int>();
70+
return pos_type(abs_pos);
71+
}
72+
73+
pos_type seekpos(pos_type pos, std::ios_base::openmode which) override {
74+
return seekoff(pos, std::ios_base::beg, which);
75+
}
76+
77+
private:
78+
py::object m_py_stream;
79+
};
80+
3581
class MemoryBuffer : public std::streambuf {
3682
public:
3783
MemoryBuffer(char* data, std::size_t size) {
@@ -54,13 +100,14 @@ class MemoryBuffer : public std::streambuf {
54100
break;
55101
default:
56102
return pos_type(off_type(-1));
57-
}
103+
}
58104
return (gptr() < eback() || gptr() > egptr()) ? pos_type(off_type(-1)) : pos_type(gptr() - eback());
59105
}
60106

61107
pos_type seekpos(pos_type pos, std::ios_base::openmode which) override {
62108
return seekoff(pos, std::ios_base::beg, which);
63109
}
110+
64111
};
65112

66113
enum class PY_TYPE : int { UNKNOWN = 0, STR, INT, FLOAT, BOOL, PARTIAL_SHAPE };

src/bindings/python/tests/test_runtime/test_compiled_model.py

+1
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ def test_compiled_model_from_buffer_in_memory(request, tmp_path, device):
314314
xml = f.read()
315315

316316
compiled = core.compile_model(model=xml, weights=weights, device_name=device)
317+
assert isinstance(compiled.outputs[0], ConstOutput)
317318
_ = compiled([np.random.normal(size=list(input.shape)).astype(dtype=input.get_element_type().to_dtype()) for input in compiled.inputs])
318319

319320

src/bindings/python/tests/test_runtime/test_core.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def test_read_model_from_tensor(request, tmp_path):
174174

175175
def test_read_model_with_wrong_input():
176176
core = Core()
177-
with pytest.raises(RuntimeError) as e:
177+
with pytest.raises(TypeError) as e:
178178
core.read_model(model=3, weights=3)
179179
assert "Provided python object type <class 'int'> isn't supported as 'model' argument." in str(e.value)
180180

@@ -228,6 +228,22 @@ def test_read_model_from_buffer(request, tmp_path):
228228
assert isinstance(model, Model)
229229

230230

231+
# request - https://docs.pytest.org/en/7.1.x/reference/reference.html#request
232+
def test_read_model_from_bytesio(request, tmp_path):
233+
from io import BytesIO
234+
235+
core = Core()
236+
xml_path, bin_path = create_filenames_for_ir(request.node.name, tmp_path)
237+
relu_model = get_relu_model()
238+
serialize(relu_model, xml_path, bin_path)
239+
with open(bin_path, "rb") as f:
240+
weights = BytesIO(f.read())
241+
with open(xml_path, "rb") as f:
242+
xml = BytesIO(f.read())
243+
model = core.read_model(model=xml, weights=weights)
244+
assert isinstance(model, Model)
245+
246+
231247
# request - https://docs.pytest.org/en/7.1.x/reference/reference.html#request
232248
def test_model_from_buffer_valid(request, tmp_path):
233249
core = Core()

0 commit comments

Comments
 (0)