tinymlc 0.1.0__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.
- TinyMLC/ANG/__init__.py +0 -0
- TinyMLC/ANG/args.py +86 -0
- TinyMLC/ANG/estimator.py +103 -0
- TinyMLC/ANG/estimator_hal.py +184 -0
- TinyMLC/ANG/estimator_qemu.py +257 -0
- TinyMLC/ANG/estimator_software.py +130 -0
- TinyMLC/ANG/model_builder.py +508 -0
- TinyMLC/ANG/model_generator.py +439 -0
- TinyMLC/ANG/model_info.py +283 -0
- TinyMLC/ANG/utils.py +420 -0
- TinyMLC/__init__.py +0 -0
- TinyMLC/cli.py +126 -0
- TinyMLC/codegen.py +877 -0
- TinyMLC/converter/__init__.py +0 -0
- TinyMLC/converter/export_weights.py +382 -0
- TinyMLC/converter/parser_litert.py +757 -0
- TinyMLC/converter/parser_onnx.py +649 -0
- TinyMLC/generate_lut.py +97 -0
- TinyMLC/handlers.py +325 -0
- TinyMLC/ops.py +76 -0
- TinyMLC/templates/lut.c.tpl +23 -0
- TinyMLC/templates/lut.h.tpl +67 -0
- TinyMLC/templates/model.c.tpl +314 -0
- TinyMLC/templates/model.h.tpl +66 -0
- TinyMLC/transform/__init__.py +0 -0
- TinyMLC/transform/algebraic.py +286 -0
- TinyMLC/transform/base.py +58 -0
- TinyMLC/transform/constant_folding.py +260 -0
- TinyMLC/transform/cse.py +192 -0
- TinyMLC/transform/dce.py +182 -0
- TinyMLC/transform/fusion.py +723 -0
- TinyMLC/transform/memory.py +200 -0
- TinyMLC/transform/pass_manager.py +101 -0
- TinyMLC/transform/simplify.py +515 -0
- tinymlc-0.1.0.dist-info/METADATA +49 -0
- tinymlc-0.1.0.dist-info/RECORD +47 -0
- tinymlc-0.1.0.dist-info/WHEEL +4 -0
- tinymlc-0.1.0.dist-info/entry_points.txt +2 -0
- tinymlc-0.1.0.dist-info/licenses/LICENSE +201 -0
- utils/__init__.py +0 -0
- utils/arm-none-eabi-gcc.cmake +53 -0
- utils/dump.py +86 -0
- utils/generate_onnx_models.py +183 -0
- utils/generate_tflite_models.py +236 -0
- utils/pack_macos.sh +88 -0
- utils/path.py +31 -0
- utils/riscv-none-elf-gcc.cmake +50 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# TinyMLC - Tiny Machine Learning Compiler
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2026 Jia Liu & TinyMLC Contributors
|
|
5
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
#
|
|
7
|
+
# This file is part of TinyMLC.
|
|
8
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
# you may not use this file except in compliance with the License.
|
|
10
|
+
# You may obtain a copy of the License at:
|
|
11
|
+
#
|
|
12
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
#
|
|
14
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
# See the License for the specific language governing permissions and
|
|
18
|
+
# limitations under the License.
|
|
19
|
+
|
|
20
|
+
# Memory reuse optimization.
|
|
21
|
+
|
|
22
|
+
from typing import Dict, Any, Tuple
|
|
23
|
+
from TinyMLC.transform.base import Pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ALIGN = 4 # Change to 8 if needed
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MemoryReuse(Pass):
|
|
30
|
+
"""
|
|
31
|
+
Memory reuse optimization.
|
|
32
|
+
|
|
33
|
+
Reuses memory buffers for tensors whose lifetimes do not overlap.
|
|
34
|
+
This reduces peak RAM usage.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, name: str = "MemoryReuse"):
|
|
38
|
+
super().__init__(name)
|
|
39
|
+
self._tensor_lifetimes: Dict[int, Tuple[int, int]] = {}
|
|
40
|
+
# idx -> (birth, death)
|
|
41
|
+
self._allocation_map: Dict[int, int] = {} # tensor_idx -> buffer_id
|
|
42
|
+
|
|
43
|
+
def run(self, model_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
44
|
+
"""Run memory reuse on model_info."""
|
|
45
|
+
model_info = self._copy_model(model_info)
|
|
46
|
+
|
|
47
|
+
# 1. Compute tensor lifetimes
|
|
48
|
+
self._compute_lifetimes(model_info)
|
|
49
|
+
|
|
50
|
+
# 2. Build interference graph
|
|
51
|
+
self._build_allocation(model_info)
|
|
52
|
+
|
|
53
|
+
# 3. Assign buffer indices
|
|
54
|
+
self._assign_buffers(model_info)
|
|
55
|
+
|
|
56
|
+
return model_info
|
|
57
|
+
|
|
58
|
+
def _compute_lifetimes(self, model_info: Dict[str, Any]) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Compute birth (first write) and death (last read) for each tensor.
|
|
61
|
+
|
|
62
|
+
Birth: when the tensor is first created (as output of some op)
|
|
63
|
+
Death: when the tensor is last used (as input to some op)
|
|
64
|
+
"""
|
|
65
|
+
ops = model_info.get("ops", [])
|
|
66
|
+
|
|
67
|
+
# Initialize: all tensors from inputs are born at time 0
|
|
68
|
+
# For simplicity, we use op indices as time markers
|
|
69
|
+
birth: Dict[int, int] = {}
|
|
70
|
+
death: Dict[int, int] = {}
|
|
71
|
+
|
|
72
|
+
# Input tensors: born at time 0
|
|
73
|
+
for inp in model_info.get("input", []):
|
|
74
|
+
idx = inp.get("tensor_index")
|
|
75
|
+
if idx is not None:
|
|
76
|
+
birth[idx] = 0
|
|
77
|
+
death[idx] = 0
|
|
78
|
+
|
|
79
|
+
# Scan ops in order
|
|
80
|
+
for op_idx, op in enumerate(ops):
|
|
81
|
+
# Outputs are born at this op index
|
|
82
|
+
for idx in op.get("output_indices", []):
|
|
83
|
+
birth[idx] = op_idx
|
|
84
|
+
death[idx] = op_idx # will be updated later
|
|
85
|
+
|
|
86
|
+
# Inputs are read at this op index
|
|
87
|
+
for idx in op.get("input_indices", []):
|
|
88
|
+
# If this is the last time this tensor is read, death = op_idx
|
|
89
|
+
# For now, we store death as the latest op that reads it
|
|
90
|
+
death[idx] = op_idx
|
|
91
|
+
|
|
92
|
+
# Also handle outputs: they live until the end
|
|
93
|
+
for out in model_info.get("output", []):
|
|
94
|
+
idx = out.get("tensor_index")
|
|
95
|
+
if idx is not None:
|
|
96
|
+
death[idx] = len(ops) # end of inference
|
|
97
|
+
|
|
98
|
+
self._tensor_lifetimes = {
|
|
99
|
+
idx: (birth.get(idx, 0), death.get(idx, 0))
|
|
100
|
+
for idx in set(birth.keys()) | set(death.keys())
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
self._log_change(
|
|
104
|
+
f"Computed lifetimes for {len(self._tensor_lifetimes)} tensors"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _build_allocation(self, model_info: Dict[str, Any]) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Build allocation map using a greedy algorithm.
|
|
110
|
+
|
|
111
|
+
Two tensors can share a buffer if their lifetimes do not overlap.
|
|
112
|
+
"""
|
|
113
|
+
tensors = list(self._tensor_lifetimes.keys())
|
|
114
|
+
allocations: Dict[int, int] = {}
|
|
115
|
+
buffer_sizes: Dict[int, int] = {} # buffer_id -> max size
|
|
116
|
+
|
|
117
|
+
# Sort tensors by birth time (earliest first)
|
|
118
|
+
sorted_tensors = sorted(
|
|
119
|
+
tensors,
|
|
120
|
+
key=lambda idx: self._tensor_lifetimes[idx][0]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for idx in sorted_tensors:
|
|
124
|
+
birth, death = self._tensor_lifetimes[idx]
|
|
125
|
+
size = self._get_tensor_size(model_info, idx)
|
|
126
|
+
|
|
127
|
+
# Find an existing buffer that can be reused
|
|
128
|
+
allocated = False
|
|
129
|
+
for buffer_id, buffer_size in buffer_sizes.items():
|
|
130
|
+
# Check if any tensor currently using this buffer overlaps
|
|
131
|
+
overlaps = False
|
|
132
|
+
for allocated_idx, buf_id in allocations.items():
|
|
133
|
+
if buf_id != buffer_id:
|
|
134
|
+
continue
|
|
135
|
+
a_birth, a_death = self._tensor_lifetimes[allocated_idx]
|
|
136
|
+
if not (death < a_birth or birth > a_death):
|
|
137
|
+
overlaps = True
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
if not overlaps and size <= buffer_size:
|
|
141
|
+
allocations[idx] = buffer_id
|
|
142
|
+
allocated = True
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
if not allocated:
|
|
146
|
+
# Create new buffer
|
|
147
|
+
buffer_id = len(buffer_sizes)
|
|
148
|
+
allocations[idx] = buffer_id
|
|
149
|
+
buffer_sizes[buffer_id] = size
|
|
150
|
+
|
|
151
|
+
self._allocation_map = allocations
|
|
152
|
+
self._log_change(
|
|
153
|
+
f"Allocated {len(buffer_sizes)} buffers for {len(tensors)} tensors"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _assign_buffers(self, model_info: Dict[str, Any]) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Assign buffer IDs to tensors in model_info.
|
|
159
|
+
|
|
160
|
+
This adds a 'buffer_id' field to each tensor spec.
|
|
161
|
+
"""
|
|
162
|
+
tensors = model_info.get("tensors", {})
|
|
163
|
+
for idx, spec in tensors.items():
|
|
164
|
+
# Add buffer_id to the tensor spec (will be used by codegen)
|
|
165
|
+
spec["buffer_id"] = self._allocation_map.get(idx, -1)
|
|
166
|
+
|
|
167
|
+
# For JSON output, we also add it to the dict representation
|
|
168
|
+
# But we don't need to expose it to codegen yet
|
|
169
|
+
|
|
170
|
+
total_bytes = sum(
|
|
171
|
+
self._get_tensor_size(model_info, idx) for idx in tensors.keys()
|
|
172
|
+
)
|
|
173
|
+
peak_bytes = sum(
|
|
174
|
+
self._get_tensor_size(model_info, idx)
|
|
175
|
+
for idx, buf_id in self._allocation_map.items()
|
|
176
|
+
if idx in tensors
|
|
177
|
+
)
|
|
178
|
+
self._log_change(
|
|
179
|
+
f"Peak RAM: {peak_bytes} bytes "
|
|
180
|
+
f"(total unique: {total_bytes})"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _get_tensor_size(self, model_info: Dict[str, Any], idx: int) -> int:
|
|
184
|
+
"""Get the size (in bytes) of a tensor."""
|
|
185
|
+
tensors = model_info.get("tensors", {})
|
|
186
|
+
spec = tensors.get(idx)
|
|
187
|
+
if not spec:
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
shape = spec.shape if hasattr(spec, 'shape') else spec.get('shape', [])
|
|
191
|
+
size = 1
|
|
192
|
+
for d in shape:
|
|
193
|
+
size *= d
|
|
194
|
+
|
|
195
|
+
# Assume 1 byte per element for int8
|
|
196
|
+
# TODO: Handle different dtypes
|
|
197
|
+
|
|
198
|
+
# Align to ALIGN bytes
|
|
199
|
+
aligned_size = ((size + ALIGN - 1) // ALIGN) * ALIGN
|
|
200
|
+
return size
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# TinyMLC - Tiny Machine Learning Compiler
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2026 Jia Liu & TinyMLC Contributors
|
|
5
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
#
|
|
7
|
+
# This file is part of TinyMLC.
|
|
8
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
# you may not use this file except in compliance with the License.
|
|
10
|
+
# You may obtain a copy of the License at:
|
|
11
|
+
#
|
|
12
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
#
|
|
14
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
# See the License for the specific language governing permissions and
|
|
18
|
+
# limitations under the License.
|
|
19
|
+
|
|
20
|
+
# Pass manager that runs a sequence of optimization passes.
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
from typing import Dict, Any, List
|
|
26
|
+
from TinyMLC.transform.base import Pass
|
|
27
|
+
from TinyMLC.transform.constant_folding import ConstantFolding
|
|
28
|
+
from TinyMLC.transform.dce import DeadCodeElimination
|
|
29
|
+
from TinyMLC.transform.cse import CommonSubexpressionElimination
|
|
30
|
+
from TinyMLC.transform.simplify import Simplify
|
|
31
|
+
from TinyMLC.transform.algebraic import AlgebraicSimplify
|
|
32
|
+
from TinyMLC.transform.fusion import OperatorFusion
|
|
33
|
+
from TinyMLC.transform.memory import MemoryReuse
|
|
34
|
+
from utils.dump import info
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PassManager:
|
|
38
|
+
"""
|
|
39
|
+
Manages and runs a sequence of optimization passes.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, passes: List[Pass] = None):
|
|
43
|
+
self.passes = passes or []
|
|
44
|
+
self._results = []
|
|
45
|
+
|
|
46
|
+
def add_pass(self, pass_obj: Pass) -> None:
|
|
47
|
+
"""Add a pass to the pipeline."""
|
|
48
|
+
self.passes.append(pass_obj)
|
|
49
|
+
|
|
50
|
+
def run(self, model_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
51
|
+
"""
|
|
52
|
+
Run all passes in sequence on model_info.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The transformed model_info after all passes.
|
|
56
|
+
"""
|
|
57
|
+
current = model_info
|
|
58
|
+
self._results = []
|
|
59
|
+
|
|
60
|
+
for pass_obj in self.passes:
|
|
61
|
+
info(f" Running: {pass_obj.name}")
|
|
62
|
+
current = pass_obj.run(current)
|
|
63
|
+
print(f"OPTIMIZED_MODEL: {json.dumps(current)}")
|
|
64
|
+
sys.stdout.flush()
|
|
65
|
+
self._results.append({
|
|
66
|
+
"name": pass_obj.name,
|
|
67
|
+
"stats": pass_obj.get_stats(),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return current
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def default_pipeline(cls) -> "PassManager":
|
|
74
|
+
"""Create the default optimization pass pipeline."""
|
|
75
|
+
pm = cls()
|
|
76
|
+
pm.add_pass(ConstantFolding())
|
|
77
|
+
pm.add_pass(DeadCodeElimination())
|
|
78
|
+
pm.add_pass(CommonSubexpressionElimination())
|
|
79
|
+
pm.add_pass(DeadCodeElimination())
|
|
80
|
+
pm.add_pass(Simplify())
|
|
81
|
+
pm.add_pass(DeadCodeElimination())
|
|
82
|
+
pm.add_pass(OperatorFusion())
|
|
83
|
+
pm.add_pass(DeadCodeElimination())
|
|
84
|
+
pm.add_pass(AlgebraicSimplify())
|
|
85
|
+
pm.add_pass(DeadCodeElimination())
|
|
86
|
+
pm.add_pass(MemoryReuse())
|
|
87
|
+
return pm
|
|
88
|
+
|
|
89
|
+
def get_results(self) -> List[Dict[str, Any]]:
|
|
90
|
+
"""Get statistics from all passes."""
|
|
91
|
+
return self._results
|
|
92
|
+
|
|
93
|
+
def dump_summary(self) -> None:
|
|
94
|
+
"""Dump a summary of all passes."""
|
|
95
|
+
info("\n=== Pass Summary ===")
|
|
96
|
+
for result in self._results:
|
|
97
|
+
stats = result["stats"]
|
|
98
|
+
changes = stats.get("changes", [])
|
|
99
|
+
info(f" {result['name']}: {len(changes)} changes")
|
|
100
|
+
for change in changes:
|
|
101
|
+
info(f" - {change}")
|