conv-allocator 3.17.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ # -------------------------------------------------------------------------------
2
+ # (c) Copyright 2025 Sony Semiconductor Israel, Ltd. All rights reserved.
3
+ #
4
+ # This software, in source or object form (the "Software"), is the
5
+ # property of Sony Semiconductor Israel Ltd. (the "Company") and/or its
6
+ # licensors, which have all right, title and interest therein, You
7
+ # may use the Software only in accordance with the terms of written
8
+ # license agreement between you and the Company (the "License").
9
+ # Except as expressly stated in the License, the Company grants no
10
+ # licenses by implication, estoppel, or otherwise. If you are not
11
+ # aware of or do not agree to the License terms, you may not use,
12
+ # copy or modify the Software. You may use the source code of the
13
+ # Software only for your internal purposes and may not distribute the
14
+ # source code of the Software, any part thereof, or any derivative work
15
+ # thereof, to any third party, except pursuant to the Company's prior
16
+ # written consent.
17
+ # The Software is the confidential information of the Company.
18
+ # -------------------------------------------------------------------------------
@@ -0,0 +1,323 @@
1
+ # -------------------------------------------------------------------------------
2
+ # (c) Copyright 2025 Sony Semiconductor Israel, Ltd. All rights reserved.
3
+ #
4
+ # This software, in source or object form (the "Software"), is the
5
+ # property of Sony Semiconductor Israel Ltd. (the "Company") and/or its
6
+ # licensors, which have all right, title and interest therein, You
7
+ # may use the Software only in accordance with the terms of written
8
+ # license agreement between you and the Company (the "License").
9
+ # Except as expressly stated in the License, the Company grants no
10
+ # licenses by implication, estoppel, or otherwise. If you are not
11
+ # aware of or do not agree to the License terms, you may not use,
12
+ # copy or modify the Software. You may use the source code of the
13
+ # Software only for your internal purposes and may not distribute the
14
+ # source code of the Software, any part thereof, or any derivative work
15
+ # thereof, to any third party, except pursuant to the Company's prior
16
+ # written consent.
17
+ # The Software is the confidential information of the Company.
18
+ # -------------------------------------------------------------------------------
19
+ import argparse
20
+ import math
21
+ import os
22
+ import csv
23
+ import json
24
+ import time
25
+ from dataclasses import dataclass
26
+ from itertools import combinations
27
+ from pathlib import Path
28
+
29
+ from ortools.sat.python import cp_model
30
+ from ortools.sat.python.cp_model import FIXED_SEARCH
31
+ from uni_model import GraphDefToUniModelConverter
32
+ from uni_model import UniModelToGraphDefConverter
33
+ from collections import defaultdict
34
+ from typing import List, Dict, Set
35
+ import json
36
+ from uni_model import UniModelPath
37
+
38
+ from conv_allocator.um_accessors import has_scratch, has_allocation, get_allocation, get_schedule, get_scratch_cut, get_cut, \
39
+ set_allocation
40
+
41
+
42
+ @dataclass(eq=False)
43
+ class Block:
44
+ layer_name: str
45
+ output_index: int
46
+ dynamic_memory: int
47
+ static_memory: int
48
+ offset: int
49
+ tensor_id: str
50
+
51
+ def __repr__(self):
52
+ return (f"Block({self.layer_name}, {self.output_index}, "
53
+ f"tensorId={self.tensor_id}, dynMem={self.dynamic_memory}, offset={self.offset})")
54
+
55
+ def __eq__(self, other):
56
+ return isinstance(other, Block) and (self.tensor_id == other.tensor_id)
57
+
58
+ def __hash__(self):
59
+ return hash((self.layer_name, self.output_index))
60
+
61
+
62
+ # Filter out layers without output and allocations
63
+ def filter_relevant_nodes(nodes):
64
+ excluded_classes = {'UniLayerConst'}
65
+
66
+ def is_relevant(node):
67
+ class_name = node.__class__.__name__
68
+ if class_name in excluded_classes:
69
+ return False
70
+ return True
71
+
72
+ return [node for node in nodes if is_relevant(node)]
73
+
74
+
75
+ def validate_allocation_data(nodes):
76
+ for node in nodes:
77
+ name = getattr(node, 'name', '<unknown>')
78
+
79
+ if not has_allocation(node):
80
+ raise ValueError(f"Node {name} missing 'allocations'")
81
+
82
+ try:
83
+ loaded_allocation = get_allocation(node)
84
+ except Exception:
85
+ raise ValueError(f"Node {name} has invalid 'allocations'")
86
+
87
+ if loaded_allocation.schedule is None:
88
+ raise ValueError(f"Node {name} missing 'schedule'")
89
+
90
+ if loaded_allocation.outputSizes is None:
91
+ raise ValueError(f"Node {name} missing 'outputSizes'")
92
+
93
+
94
+ def down_scale(out, scale):
95
+ return math.ceil(out.dynamicMemory/scale)
96
+
97
+ def parse_blocks_from_node(node, scale):
98
+ allocations_data = get_allocation(node)
99
+ blocks = [
100
+ Block(
101
+ out.layerName,
102
+ out.outputIndex,
103
+ down_scale(out, scale),
104
+ out.staticMemory,
105
+ out.offset,
106
+ out.tensorId
107
+ )
108
+ for out in allocations_data.outputSizes
109
+ ]
110
+ return blocks
111
+
112
+
113
+ def create_fake_scratch_node(real_node):
114
+ class FakeScratchNode:
115
+ def __init__(self, source_node):
116
+ self.source_real_node = source_node
117
+ self.name = f"{source_node.name}_scratch"
118
+
119
+ return FakeScratchNode(real_node)
120
+
121
+
122
+ def build_graph_cuts(unimodel, scale):
123
+ graph = unimodel.uni_graphs[0]
124
+ filtered_nodes = filter_relevant_nodes(graph.layer_nodes)
125
+ validate_allocation_data(filtered_nodes)
126
+
127
+ # Step 1: Sort by schedule
128
+ sorted_nodes = sorted(filtered_nodes, key=get_schedule)
129
+
130
+ # Step 2: Parse blocks from sorted nodes
131
+ all_blocks_by_node = {}
132
+ blocks_map = {}
133
+ for node in sorted_nodes:
134
+ blocks = parse_blocks_from_node(node, scale)
135
+ all_blocks_by_node[node] = blocks
136
+ for block in blocks:
137
+ blocks_map[block] = node
138
+
139
+ # Step 3: Handle scratch layers, preserving order
140
+ scratch_fake_nodes = []
141
+ ordered_nodes_with_scratch = []
142
+
143
+ for node in sorted_nodes:
144
+ ordered_nodes_with_scratch.append(node)
145
+ if has_scratch(node):
146
+ scratch_node = create_fake_scratch_node(node)
147
+ scratch_fake_nodes.append(scratch_node)
148
+ ordered_nodes_with_scratch.append(scratch_node)
149
+
150
+ # Step 4: Use precomputed graph cuts from node metadata
151
+ tensor_id_to_block = {block.tensor_id: block for block in blocks_map}
152
+ graph_cuts = []
153
+
154
+ for node in ordered_nodes_with_scratch:
155
+ if hasattr(node, 'source_real_node'): # Scratch fake node
156
+ real_node = node.source_real_node
157
+ scratch_ids = get_scratch_cut(real_node)
158
+ block_set = {tensor_id_to_block[tid] for tid in scratch_ids}
159
+ else:
160
+ cut_ids = get_cut(node)
161
+ block_set = {tensor_id_to_block[tid] for tid in cut_ids}
162
+
163
+ graph_cuts.append(block_set)
164
+
165
+ return graph_cuts, blocks_map
166
+
167
+
168
+ def solve_allocation(graph_cuts, blocks, time_limit):
169
+ """
170
+ Solve the memory allocation problem using CP-SAT solver.
171
+
172
+ Args:
173
+ graph_cuts: List of sets, each set contains block IDs that need to be in memory simultaneously
174
+ blocks: Dictionary mapping block ID to its size
175
+
176
+ Returns:
177
+ Dictionary mapping block ID to (start_time, end_time)
178
+ """
179
+
180
+ start_time = time.time()
181
+
182
+ # Estimate horizon (safe upper bound on end time)
183
+ horizon = int(sum(block.dynamic_memory for block in blocks) * 2)
184
+
185
+ model = cp_model.CpModel()
186
+
187
+ # Create variables for each block
188
+ block_dict = {}
189
+ for block in blocks.keys():
190
+ block_id = block.tensor_id
191
+ block_size = block.dynamic_memory
192
+
193
+ start_var = model.NewIntVar(0, horizon, f"start_{block_id}")
194
+ end_var = model.NewIntVar(0, horizon, f"end_{block_id}")
195
+ interval_var = model.NewIntervalVar(start_var, block_size, end_var, f"interval_{block_id}")
196
+
197
+ block_dict[block] = (start_var, end_var, interval_var)
198
+
199
+ # Solver setup
200
+ solver = cp_model.CpSolver()
201
+ solver.parameters.max_time_in_seconds = time_limit
202
+ solver.parameters.random_seed = 17
203
+ solver.parameters.randomize_search = False
204
+ solver.parameters.use_absl_random = False
205
+ solver.parameters.num_workers = 1
206
+
207
+ # fixed strategy - works with fixed_search
208
+ # activation_vars = [start for start, _, _ in block_dict.values()]
209
+ # model.AddDecisionStrategy(
210
+ # activation_vars,
211
+ # cp_model.CHOOSE_LOWEST_MIN,
212
+ # cp_model.SELECT_MIN_VALUE
213
+ # )
214
+ # solver.parameters.search_branching = cp_model.FIXED_SEARCH
215
+
216
+
217
+ # Use AddNoOverlap per graph cut
218
+ for cut in graph_cuts:
219
+ cut_intervals = [block_dict[block][2] for block in cut]
220
+ model.AddNoOverlap(cut_intervals)
221
+
222
+ # Objective: minimize makespan (max end address)
223
+ obj_var = model.NewIntVar(0, horizon, "makespan")
224
+ model.AddMaxEquality(obj_var, [end for _, end, _ in block_dict.values()])
225
+ model.Minimize(obj_var)
226
+
227
+ # Solution callback
228
+ class SolutionCallback(cp_model.CpSolverSolutionCallback):
229
+ def __init__(self):
230
+ super().__init__()
231
+ self._solution_count = 0
232
+ self.latest_solution = None
233
+
234
+ def on_solution_callback(self):
235
+ self._solution_count += 1
236
+ self.latest_solution = {
237
+ block_id: (self.Value(start), self.Value(end))
238
+ for block_id, (start, end, _) in block_dict.items()
239
+ }
240
+
241
+ callback = SolutionCallback()
242
+
243
+ status = solver.Solve(model, callback)
244
+ solve_time = time.time() - start_time
245
+
246
+ results = callback.latest_solution if callback.latest_solution else {
247
+ block_id: (solver.Value(start), solver.Value(end))
248
+ for block_id, (start, end, _) in block_dict.items()
249
+ } if status in [cp_model.OPTIMAL, cp_model.FEASIBLE] else {}
250
+
251
+ if status in [cp_model.OPTIMAL, cp_model.FEASIBLE]:
252
+ peak_memory = max(end for _, end in results.values())
253
+ print(f"Status: {solver.StatusName(status)}")
254
+ # print(f"Objective value: {solver.ObjectiveValue()}")
255
+ # print(f"Peak memory usage: {peak_memory}")
256
+ return results, solver, peak_memory, solve_time, True
257
+ else:
258
+ print(f"No solution found. Status: {solver.StatusName(status)}")
259
+ return results, solver, 0, solve_time, False
260
+
261
+ def main():
262
+ parser = argparse.ArgumentParser(description="Memory allocation solver.")
263
+ parser.add_argument("--input_path", required=True, help="Path to input file")
264
+ parser.add_argument("--output_path", required=True, help="Path to output file")
265
+ parser.add_argument("--time_limit", type=int, required=True, help="Time limit in seconds")
266
+ parser.add_argument("--scale", type=int, required=True, help="block size scale factor for address alignment.")
267
+ args = parser.parse_args()
268
+
269
+ scale = args.scale
270
+ uni_model_loaded = GraphDefToUniModelConverter.convert(args.input_path)
271
+ graph_cuts, blocks_map = build_graph_cuts(uni_model_loaded, scale)
272
+ results_scaled, allocation, max_address_scaled, solve_time, solved = solve_allocation(graph_cuts, blocks_map, args.time_limit)
273
+ max_address = max_address_scaled * scale
274
+
275
+ if solved:
276
+ layer_to_block_map = defaultdict(list)
277
+ for k, v in blocks_map.items():
278
+ layer_to_block_map[v].append(k)
279
+
280
+ block_id_to_offset = defaultdict(list)
281
+ for block in results_scaled:
282
+ offset = results_scaled[block][0] * scale
283
+ block_id_to_offset[block.tensor_id] = offset
284
+
285
+ for node in uni_model_loaded.uni_graphs[0].layer_nodes:
286
+ if has_allocation(node):
287
+ # Load allocation object from accessor
288
+ alloc_data = get_allocation(node)
289
+
290
+ # Update the outputSizes offsets inside AllocationDataPerLayer
291
+ for output_alloc in alloc_data.outputSizes:
292
+ output_alloc.offset = block_id_to_offset[output_alloc.tensorId]
293
+
294
+ # Write updated allocation object back to node
295
+ set_allocation(node, alloc_data)
296
+
297
+ # debug print addresses
298
+ # for node in uni_model_loaded.uni_graphs[0].layer_nodes:
299
+ # if has_allocation(node):
300
+ # alloc_data = get_allocation(node)
301
+ # for output_alloc in alloc_data.outputSizes:
302
+ # output_alloc.offset = block_id_to_offset[output_alloc.tensorId]
303
+ # print(f"tid: {output_alloc.tensorId}, offset: {output_alloc.offset}")
304
+
305
+ print("Solution found")
306
+ print(f"max_address: {max_address}")
307
+ print(f"solve_time: {solve_time}")
308
+ else:
309
+ print("No solution found")
310
+
311
+ path = Path(args.output_path)
312
+
313
+ directory = str(path.parent)
314
+ ext = ".um.pb"
315
+ filename_wo_ext = os.path.basename(path)
316
+ if filename_wo_ext.endswith(ext):
317
+ filename_wo_ext = filename_wo_ext[:-len(ext)]
318
+
319
+ uniPath = UniModelPath(directory, filename_wo_ext)
320
+ UniModelToGraphDefConverter.convert(uni_model_loaded, uniPath)
321
+
322
+ if __name__ == "__main__":
323
+ main()
@@ -0,0 +1,57 @@
1
+ # -------------------------------------------------------------------------------
2
+ # (c) Copyright 2025 Sony Semiconductor Israel, Ltd. All rights reserved.
3
+ #
4
+ # This software, in source or object form (the "Software"), is the
5
+ # property of Sony Semiconductor Israel Ltd. (the "Company") and/or its
6
+ # licensors, which have all right, title and interest therein, You
7
+ # may use the Software only in accordance with the terms of written
8
+ # license agreement between you and the Company (the "License").
9
+ # Except as expressly stated in the License, the Company grants no
10
+ # licenses by implication, estoppel, or otherwise. If you are not
11
+ # aware of or do not agree to the License terms, you may not use,
12
+ # copy or modify the Software. You may use the source code of the
13
+ # Software only for your internal purposes and may not distribute the
14
+ # source code of the Software, any part thereof, or any derivative work
15
+ # thereof, to any third party, except pursuant to the Company's prior
16
+ # written consent.
17
+ # The Software is the confidential information of the Company.
18
+ # -------------------------------------------------------------------------------
19
+ import sys
20
+ import argparse
21
+ from ortools.linear_solver import pywraplp
22
+
23
+ def parse_args(args=None):
24
+ parser = argparse.ArgumentParser(description='Allocation optimizer')
25
+ parser.add_argument('--source', type=str, help='Source filename', default='')
26
+ parser.add_argument('--target', type=str, help='Target filename', default='')
27
+ parser.add_argument('--time', type=int, help='Allowed time in seconds', default=0)
28
+
29
+ return parser.parse_args(args)
30
+
31
+ def hello_world(source_filename, target_filename, allowed_time):
32
+ print(f"Hello from conv-allocator!")
33
+ print(f"Source: {source_filename}")
34
+ print(f"Target: {target_filename}")
35
+ print(f"Allowed time: {allowed_time} seconds")
36
+
37
+ solver = pywraplp.Solver.CreateSolver('GLOP')
38
+ x = solver.NumVar(0, 10, 'x')
39
+ y = solver.NumVar(0, 10, 'y')
40
+ solver.Add(x + y <= 20)
41
+ solver.Maximize(x + y)
42
+ status = solver.Solve()
43
+ if status == pywraplp.Solver.OPTIMAL:
44
+ print(f"x = {x.solution_value()}, y = {y.solution_value()}")
45
+ else:
46
+ print("No solution found")
47
+
48
+ def main(args=None):
49
+ if args is None:
50
+ # Use sys.argv if no args provided
51
+ args = sys.argv[1:]
52
+
53
+ parsed_args = parse_args(args)
54
+ hello_world(parsed_args.source, parsed_args.target, parsed_args.time)
55
+
56
+ if __name__ == "__main__":
57
+ main()
@@ -0,0 +1,113 @@
1
+ # -------------------------------------------------------------------------------
2
+ # (c) Copyright 2025 Sony Semiconductor Israel, Ltd. All rights reserved.
3
+ #
4
+ # This software, in source or object form (the "Software"), is the
5
+ # property of Sony Semiconductor Israel Ltd. (the "Company") and/or its
6
+ # licensors, which have all right, title and interest therein, You
7
+ # may use the Software only in accordance with the terms of written
8
+ # license agreement between you and the Company (the "License").
9
+ # Except as expressly stated in the License, the Company grants no
10
+ # licenses by implication, estoppel, or otherwise. If you are not
11
+ # aware of or do not agree to the License terms, you may not use,
12
+ # copy or modify the Software. You may use the source code of the
13
+ # Software only for your internal purposes and may not distribute the
14
+ # source code of the Software, any part thereof, or any derivative work
15
+ # thereof, to any third party, except pursuant to the Company's prior
16
+ # written consent.
17
+ # The Software is the confidential information of the Company.
18
+ # -------------------------------------------------------------------------------
19
+ import json
20
+ from dataclasses import dataclass, field
21
+ from json import JSONEncoder, dumps
22
+ from typing import Optional, List
23
+ from uni_model import UniModel, UniGraph, UniLayer
24
+
25
+ ALLOCATIONS = 'allocations'
26
+ HAS_SCRATCH = 'hasScratch'
27
+ OUTPUT_SIZES = 'outputSizes'
28
+
29
+
30
+ def has_scratch(layer: UniLayer):
31
+ return layer.extended_attr.get(HAS_SCRATCH, False)
32
+
33
+
34
+ def has_allocation(layer: UniLayer):
35
+ return layer.extended_attr.get(ALLOCATIONS, False)
36
+
37
+
38
+ def get_allocation(layer: UniLayer):
39
+ return AllocationDataPerLayer.decode_from_json(layer.extended_attr.get(ALLOCATIONS))
40
+
41
+
42
+
43
+
44
+ def get_schedule(node: UniLayer):
45
+ if has_allocation(node):
46
+ return get_allocation(node).schedule
47
+ else:
48
+ raise ValueError(f"Node {node.name} missing 'allocation'")
49
+
50
+
51
+ def get_scratch_cut(node: UniLayer):
52
+ if has_allocation(node):
53
+ return get_allocation(node).scratchCutTensorIds
54
+ else:
55
+ raise ValueError(f"Node {node.name} missing 'allocation'")
56
+
57
+
58
+ def get_cut(node: UniLayer):
59
+ if has_allocation(node):
60
+ return get_allocation(node).cutTensorIds
61
+ else:
62
+ raise ValueError(f"Node {node.name} missing 'allocation'")
63
+
64
+
65
+ @dataclass
66
+ class OutputAllocation:
67
+ tensorId: str
68
+ layerName: str
69
+ outputIndex: int
70
+ dynamicMemory: int
71
+ staticMemory: int
72
+ offset: Optional[int] = None
73
+
74
+
75
+ @dataclass
76
+ class AllocationDataPerLayer:
77
+ schedule: int
78
+ dynamicMemory: int
79
+ cutTensorIds: list[str] = field(default_factory=list)
80
+ scratchCutTensorIds: list[str] = field(default_factory=list)
81
+ outputSizes: list[OutputAllocation] = field(default_factory=list)
82
+
83
+ def encode_to_json(self) -> str:
84
+ d = self.__dict__.copy()
85
+ d[OUTPUT_SIZES] = [o.__dict__ for o in d[OUTPUT_SIZES]]
86
+ return json.dumps(d)
87
+
88
+ @staticmethod
89
+ def decode_from_json(j: str) -> 'AllocationDataPerLayer':
90
+ d = json.loads(j)
91
+ d[OUTPUT_SIZES] = [OutputAllocation(**o) for o in d[OUTPUT_SIZES]]
92
+ return AllocationDataPerLayer(**d)
93
+
94
+
95
+ def set_allocation(layer: UniLayer, alloc_data: AllocationDataPerLayer):
96
+ layer.extended_attr[ALLOCATIONS] = alloc_data.encode_to_json()
97
+
98
+
99
+ if __name__ == "__main__":
100
+ a = AllocationDataPerLayer(
101
+ schedule=4,
102
+ dynamicMemory=9,
103
+ cutTensorIds=["t1", "t2"],
104
+ scratchCutTensorIds=["t2"],
105
+ outputSizes=[
106
+ OutputAllocation(tensorId="t1", layerName="layerX", outputIndex=0, dynamicMemory=10, staticMemory=5)
107
+ ]
108
+ )
109
+ encoded = a.encode_to_json()
110
+ print("Serialized:", encoded)
111
+
112
+ decoded = AllocationDataPerLayer.decode_from_json(encoded)
113
+ print("Deserialized:", decoded)
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.1
2
+ Name: conv-allocator
3
+ Version: 3.17.1
4
+ Requires-Dist: ortools==9.9.3963
5
+ Requires-Dist: protobuf==4.25.5
6
+ Requires-Dist: uni-model==10.0.23
7
+
@@ -0,0 +1,9 @@
1
+ conv_allocator/__init__.py,sha256=phGsoy1jdNsCmzQFKZEWB_Thbw633GKNXRHR4TKs_0w,1208
2
+ conv_allocator/allocator.py,sha256=aXp_obbxqtrK3wq57N0WmUY2wUXcn2HdegOuxro_K20,11710
3
+ conv_allocator/allocator_test.py,sha256=sCWtHJKG2kH2f_ppRdNALwhgucK-mWEJyM-JfnGkN3U,2524
4
+ conv_allocator/um_accessors.py,sha256=-6AIZ3x53ILeLBZCsIMI2b-ywslwWGd-QWK7RCy7n9c,3832
5
+ conv_allocator-3.17.1.dist-info/METADATA,sha256=hTlJGQXJS3iwodf0njPo_fbtWENquKMPNaDWjzkE0lk,159
6
+ conv_allocator-3.17.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
7
+ conv_allocator-3.17.1.dist-info/entry_points.txt,sha256=hLQ1x1lLRtbJWuJG8CtSpOdaN28hbYAVd_07szV-cKU,64
8
+ conv_allocator-3.17.1.dist-info/top_level.txt,sha256=JWue8wV0JXEagm9a0qky3c1sKcImcUrQoJ_JskNFiqQ,15
9
+ conv_allocator-3.17.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.45.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ run-allocator = conv_allocator.allocator:main
@@ -0,0 +1 @@
1
+ conv_allocator