Skip to content

Commit 8427701

Browse files
authored
Delayed constant serialization (#29106)
### Details: - New PostponedConstantReplacer class to wrap nodes in serialization. If a node has "postponed_constant " rt info property, it will be substituted with a new constant, got from evaluate. PostponedConstantReplacer and new constant objects to be deleted after usage. - New python PostponedConstant operation which does not has an initial value and calculates in via callback on evaluate call - Some fixes in serialization - C++ and python tests ### Tickets: - CVS-162940
1 parent a757e87 commit 8427701

File tree

10 files changed

+287
-42
lines changed

10 files changed

+287
-42
lines changed

src/bindings/python/src/openvino/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@
5252
from openvino._pyopenvino import RemoteTensor
5353

5454
# Import public classes from _ov_api
55+
from openvino._op_base import Op
5556
from openvino._ov_api import Model
5657
from openvino._ov_api import Core
5758
from openvino._ov_api import CompiledModel
5859
from openvino._ov_api import InferRequest
5960
from openvino._ov_api import AsyncInferQueue
60-
from openvino._ov_api import Op
6161

6262
# Import all public modules
6363
from openvino.package_utils import LazyLoader
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (C) 2018-2025 Intel Corporation
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
from typing import Union, Optional, Tuple, List
6+
7+
from openvino._pyopenvino import Op as OpBase
8+
from openvino._pyopenvino import Node, Output
9+
10+
11+
class Op(OpBase):
12+
def __init__(self, py_obj: "Op", inputs: Optional[Union[List[Union[Node, Output]], Tuple[Union[Node, Output, List[Union[Node, Output]]]]]] = None) -> None:
13+
super().__init__(py_obj)
14+
self._update_type_info()
15+
if isinstance(inputs, tuple):
16+
inputs = None if len(inputs) == 0 else list(inputs)
17+
if inputs is not None and len(inputs) == 1 and isinstance(inputs[0], list):
18+
inputs = inputs[0]
19+
if inputs is not None:
20+
self.set_arguments(inputs)
21+
self.constructor_validate_and_infer_types()

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

+2-16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import io
66
from types import TracebackType
7-
from typing import Any, Iterable, Union, Optional, Dict, Tuple, List
7+
from typing import Any, Iterable, Union, Optional, Dict
88
from typing import Type as TypingType
99
from pathlib import Path
1010

@@ -13,8 +13,7 @@
1313
from openvino._pyopenvino import Core as CoreBase
1414
from openvino._pyopenvino import CompiledModel as CompiledModelBase
1515
from openvino._pyopenvino import AsyncInferQueue as AsyncInferQueueBase
16-
from openvino._pyopenvino import Op as OpBase
17-
from openvino._pyopenvino import Node, Output, Tensor, Type
16+
from openvino._pyopenvino import Node, Tensor, Type
1817

1918
from openvino.utils.data_helpers import (
2019
OVDict,
@@ -25,19 +24,6 @@
2524
from openvino.package_utils import deprecatedclassproperty
2625

2726

28-
class Op(OpBase):
29-
def __init__(self, py_obj: "Op", inputs: Optional[Union[List[Union[Node, Output]], Tuple[Union[Node, Output, List[Union[Node, Output]]]]]] = None) -> None:
30-
super().__init__(py_obj)
31-
self._update_type_info()
32-
if isinstance(inputs, tuple):
33-
inputs = None if len(inputs) == 0 else list(inputs)
34-
if inputs is not None and len(inputs) == 1 and isinstance(inputs[0], list):
35-
inputs = inputs[0]
36-
if inputs is not None:
37-
self.set_arguments(inputs)
38-
self.constructor_validate_and_infer_types()
39-
40-
4127
class ModelMeta(type):
4228
def __dir__(cls) -> list:
4329
return list(set(cls.__dict__.keys()) | set(dir(ModelBase)))

src/bindings/python/src/openvino/utils/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
from openvino.package_utils import deprecated
1111
from openvino.package_utils import classproperty
1212
from openvino.package_utils import deprecatedclassproperty
13+
from openvino.utils.postponed_constant import make_postponed_constant
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (C) 2018-2025 Intel Corporation
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
6+
from typing import Callable, List, Optional
7+
from openvino import Op, Type, Shape, Tensor, PartialShape
8+
9+
10+
class PostponedConstant(Op):
11+
"""Postponed Constant is a way to materialize a big constant only when it is going to be serialized to IR and then immediately dispose."""
12+
def __init__(self, element_type: Type, shape: Shape, maker: Callable[[Tensor], None], name: Optional[str] = None) -> None:
13+
super().__init__(self)
14+
self.get_rt_info()["postponed_constant"] = True # value doesn't matter
15+
self.m_element_type = element_type
16+
self.m_shape = shape
17+
self.m_maker = maker
18+
if name is not None:
19+
self.friendly_name = name
20+
self.constructor_validate_and_infer_types()
21+
22+
def evaluate(self, outputs: List[Tensor], _: List[Tensor]) -> bool:
23+
self.m_maker(outputs[0])
24+
return True
25+
26+
def validate_and_infer_types(self) -> None:
27+
self.set_output_type(0, self.m_element_type, PartialShape(self.m_shape))
28+
29+
def clone_with_new_inputs(self, new_inputs: List[Tensor]) -> Op:
30+
return PostponedConstant(self.m_element_type, self.m_shape, self.m_maker, self.friendly_name)
31+
32+
def has_evaluate(self) -> bool:
33+
return True
34+
35+
36+
# `maker` is a function that returns ov.Tensor that represents a target Constant
37+
def make_postponed_constant(element_type: Type, shape: Shape, maker: Callable[[Tensor], None], name: Optional[str] = None) -> Op:
38+
return PostponedConstant(element_type, shape, maker, name)

src/bindings/python/tests/test_utils/test_utils.py

+96-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import pytest
66
import os
7+
import numpy as np
8+
import openvino as ov
79
from pathlib import Path
8-
from openvino.utils import deprecated, get_cmake_path
9-
from tests.utils.helpers import compare_models, get_relu_model
10+
from openvino.utils import deprecated, get_cmake_path, make_postponed_constant
11+
from tests.utils.helpers import compare_models, get_relu_model, create_filenames_for_ir
1012

1113

1214
def test_compare_functions():
@@ -94,3 +96,95 @@ def mock_walk(path):
9496
result = get_cmake_path()
9597

9698
assert result == ""
99+
100+
101+
class Maker:
102+
def __init__(self):
103+
self.calls_count = 0
104+
105+
def __call__(self, tensor: ov.Tensor) -> None:
106+
self.calls_count += 1
107+
tensor_data = np.array([2, 2, 2, 2], dtype=np.float32).reshape(1, 1, 2, 2)
108+
ov.Tensor(tensor_data).copy_to(tensor)
109+
110+
def called_times(self):
111+
return self.calls_count
112+
113+
114+
def create_model(maker):
115+
input_shape = ov.Shape([1, 2, 1, 2])
116+
param_node = ov.opset13.parameter(input_shape, ov.Type.f32, name="data")
117+
118+
postponned_constant = make_postponed_constant(ov.Type.f32, input_shape, maker)
119+
120+
add_1 = ov.opset13.add(param_node, postponned_constant)
121+
122+
const_2 = ov.op.Constant(ov.Type.f32, input_shape, [1, 2, 3, 4])
123+
add_2 = ov.opset13.add(add_1, const_2)
124+
125+
return ov.Model(add_2, [param_node], "test_model")
126+
127+
128+
@pytest.fixture
129+
def prepare_ir_paths(request, tmp_path):
130+
xml_path, bin_path = create_filenames_for_ir(request.node.name, tmp_path)
131+
132+
yield xml_path, bin_path
133+
134+
# IR Files deletion should be done after `Model` is destructed.
135+
# It may be achieved by splitting scopes (`Model` will be destructed
136+
# just after test scope finished), or by calling `del Model`
137+
os.remove(xml_path)
138+
os.remove(bin_path)
139+
140+
141+
def test_save_postponned_constant(prepare_ir_paths):
142+
maker = Maker()
143+
model = create_model(maker)
144+
assert maker.called_times() == 0
145+
146+
model_export_file_name, weights_export_file_name = prepare_ir_paths
147+
ov.save_model(model, model_export_file_name, compress_to_fp16=False)
148+
149+
assert maker.called_times() == 1
150+
151+
152+
def test_save_postponned_constant_twice(prepare_ir_paths):
153+
maker = Maker()
154+
model = create_model(maker)
155+
assert maker.called_times() == 0
156+
157+
model_export_file_name, weights_export_file_name = prepare_ir_paths
158+
ov.save_model(model, model_export_file_name, compress_to_fp16=False)
159+
assert maker.called_times() == 1
160+
ov.save_model(model, model_export_file_name, compress_to_fp16=False)
161+
assert maker.called_times() == 2
162+
163+
164+
def test_serialize_postponned_constant(prepare_ir_paths):
165+
maker = Maker()
166+
model = create_model(maker)
167+
assert maker.called_times() == 0
168+
169+
model_export_file_name, weights_export_file_name = prepare_ir_paths
170+
ov.serialize(model, model_export_file_name, weights_export_file_name)
171+
assert maker.called_times() == 1
172+
173+
174+
def test_infer_postponned_constant():
175+
maker = Maker()
176+
model = create_model(maker)
177+
assert maker.called_times() == 0
178+
179+
compiled_model = ov.compile_model(model, "CPU")
180+
assert isinstance(compiled_model, ov.CompiledModel)
181+
182+
request = compiled_model.create_infer_request()
183+
input_data = np.ones([1, 2, 1, 2], dtype=np.float32)
184+
input_tensor = ov.Tensor(input_data)
185+
186+
results = request.infer({"data": input_tensor})
187+
assert maker.called_times() == 1
188+
189+
expected_output = np.array([4, 5, 6, 7], dtype=np.float32).reshape(1, 2, 1, 2)
190+
assert np.allclose(results[list(results)[0]], expected_output, 1e-4, 1e-4)

0 commit comments

Comments
 (0)