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
TinyMLC/ANG/__init__.py
ADDED
|
File without changes
|
TinyMLC/ANG/args.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
#!/usr/bin/env python3
|
|
21
|
+
"""Command-line argument utilities for ANG and TinyMLC CLI"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
from typing import List
|
|
25
|
+
|
|
26
|
+
from TinyMLC.ANG.estimator import Estimator
|
|
27
|
+
from TinyMLC.ANG.estimator_software import SoftwareEstimator
|
|
28
|
+
from TinyMLC.ANG.estimator_qemu import QemuEstimator
|
|
29
|
+
from TinyMLC.ANG.estimator_hal import HardwareHALEstimator
|
|
30
|
+
from utils.dump import fatal_error
|
|
31
|
+
|
|
32
|
+
def parse_shape(shape_str: str) -> List[int]:
|
|
33
|
+
"""Parse shape string like '1,28,28,1' to list of integers"""
|
|
34
|
+
return [int(x.strip()) for x in shape_str.split(",")]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_estimator(args: argparse.Namespace) -> Estimator:
|
|
38
|
+
"""
|
|
39
|
+
Create an estimator based on command-line arguments.
|
|
40
|
+
"""
|
|
41
|
+
estimator_type = getattr(args, "estimator", "software")
|
|
42
|
+
|
|
43
|
+
# Read from args
|
|
44
|
+
max_macs = getattr(args, "max_macs", 100000)
|
|
45
|
+
max_ram_kb = getattr(args, "max_ram", 30)
|
|
46
|
+
max_flash_kb = getattr(args, "max_flash", 64)
|
|
47
|
+
|
|
48
|
+
if estimator_type == "software":
|
|
49
|
+
return SoftwareEstimator(
|
|
50
|
+
{
|
|
51
|
+
"max_macs": max_macs,
|
|
52
|
+
"max_params": 50000, # TODO: Independently configurable.
|
|
53
|
+
"max_ram": max_ram_kb * 1024,
|
|
54
|
+
"clock_speed": getattr(args, "clock_speed", 100000000),
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
elif estimator_type == "qemu":
|
|
59
|
+
return QemuEstimator(
|
|
60
|
+
{
|
|
61
|
+
"max_macs": max_macs,
|
|
62
|
+
"max_params": 50000,
|
|
63
|
+
"max_ram": max_ram_kb * 1024,
|
|
64
|
+
"qemu_binary": "qemu-system-" + getattr(args, "target", "arm"),
|
|
65
|
+
"cpu": getattr(args, "qemu_cpu", "cortex-m4"),
|
|
66
|
+
"icount_shift": getattr(args, "icount_shift", 0),
|
|
67
|
+
"clock_speed": getattr(args, "clock_speed", 100000000),
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
elif estimator_type == "hardware":
|
|
72
|
+
return HardwareHALEstimator(
|
|
73
|
+
{
|
|
74
|
+
"max_macs": max_macs,
|
|
75
|
+
"max_params": 50000,
|
|
76
|
+
"max_ram": max_ram_kb * 1024,
|
|
77
|
+
"script_path": getattr(args, "estimator_script", None),
|
|
78
|
+
"function_name": getattr(
|
|
79
|
+
args, "estimator_function", "estimate"
|
|
80
|
+
),
|
|
81
|
+
"clock_speed": getattr(args, "clock_speed", 100000000),
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
else:
|
|
86
|
+
fatal_error(f"Unknown estimator type: {estimator_type}")
|
TinyMLC/ANG/estimator.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
# Abstract base class for all estimators.
|
|
21
|
+
|
|
22
|
+
from abc import ABC, abstractmethod
|
|
23
|
+
from typing import Dict, Any, Optional
|
|
24
|
+
|
|
25
|
+
from TinyMLC.ANG.utils import hash_structure
|
|
26
|
+
|
|
27
|
+
class Estimator(ABC):
|
|
28
|
+
"""
|
|
29
|
+
Abstract base class for network performance estimation.
|
|
30
|
+
|
|
31
|
+
All estimators (Software, QEMU, Hardware HAL) inherit from this class
|
|
32
|
+
and implement the estimate() method.
|
|
33
|
+
|
|
34
|
+
The estimate() method takes a model_info structure and returns
|
|
35
|
+
a Score dictionary with performance metrics.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
39
|
+
"""
|
|
40
|
+
Initialize the estimator with optional configuration.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
config: Configuration dictionary for the estimator.
|
|
44
|
+
"""
|
|
45
|
+
self.config = config or {}
|
|
46
|
+
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def estimate(self, model_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Estimate the performance of a given model.
|
|
52
|
+
|
|
53
|
+
The returned dictionary must contain at least:
|
|
54
|
+
- 'score': float, higher is better
|
|
55
|
+
- 'macs': int
|
|
56
|
+
- 'params': int
|
|
57
|
+
- 'peak_ram': int (bytes)
|
|
58
|
+
- 'flash': int (bytes)
|
|
59
|
+
- 'latency_ms': float
|
|
60
|
+
|
|
61
|
+
Additional fields can be added as needed.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
model_info: ModelInfo dictionary.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dictionary with performance metrics.
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def get_info(self) -> Dict[str, str]:
|
|
73
|
+
"""
|
|
74
|
+
Get information about this estimator.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dictionary with estimator name, version, and description.
|
|
78
|
+
"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
def estimate_cached(self, model_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Estimate with caching to avoid redundant evaluations.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
model_info: ModelInfo dictionary.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Dictionary with performance metrics.
|
|
90
|
+
"""
|
|
91
|
+
# Compute a hash of the model structure
|
|
92
|
+
key = hash_structure(model_info)
|
|
93
|
+
|
|
94
|
+
if key in self._cache:
|
|
95
|
+
return self._cache[key]
|
|
96
|
+
|
|
97
|
+
result = self.estimate(model_info)
|
|
98
|
+
self._cache[key] = result
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
def clear_cache(self) -> None:
|
|
102
|
+
"""Clear the evaluation cache."""
|
|
103
|
+
self._cache.clear()
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
# Hardware HAL estimator - user-provided script for real hardware.
|
|
21
|
+
|
|
22
|
+
import importlib.util
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Dict, Any, Optional
|
|
26
|
+
|
|
27
|
+
from TinyMLC.ANG.estimator import Estimator
|
|
28
|
+
from TinyMLC.ANG.utils import (calculate_macs, calculate_params,
|
|
29
|
+
calculate_peak_ram)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HardwareHALEstimator(Estimator):
|
|
33
|
+
"""
|
|
34
|
+
Hardware HAL estimator (closed-loop).
|
|
35
|
+
|
|
36
|
+
This estimator calls a user-provided Python script that interfaces
|
|
37
|
+
with real hardware. The script must implement the estimate() function.
|
|
38
|
+
|
|
39
|
+
This is a "closed-loop" estimator because it provides feedback
|
|
40
|
+
from actual hardware execution.
|
|
41
|
+
|
|
42
|
+
Vendors can implement their own hardware testing scripts following
|
|
43
|
+
the same interface.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
47
|
+
"""
|
|
48
|
+
Initialize the hardware HAL estimator.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
config: Must contain 'script_path' pointing to the user script.
|
|
52
|
+
"""
|
|
53
|
+
super().__init__(config)
|
|
54
|
+
|
|
55
|
+
self.default_config = {
|
|
56
|
+
"script_path": None, # Path to user script
|
|
57
|
+
"function_name": "estimate", # Function name in user script
|
|
58
|
+
"timeout": 60, # Timeout in seconds
|
|
59
|
+
"max_macs": 100000,
|
|
60
|
+
"max_params": 50000,
|
|
61
|
+
"max_ram": 32768,
|
|
62
|
+
}
|
|
63
|
+
self.config = {**self.default_config, **(config or {})}
|
|
64
|
+
self._estimator_func = None
|
|
65
|
+
|
|
66
|
+
if self.config.get("script_path"):
|
|
67
|
+
self._load_estimator()
|
|
68
|
+
|
|
69
|
+
def _load_estimator(self) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Load the user-provided estimator function.
|
|
72
|
+
"""
|
|
73
|
+
script_path = self.config.get("script_path")
|
|
74
|
+
if not script_path:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
"HardwareHALEstimator requires 'script_path' in config"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
path = Path(script_path)
|
|
80
|
+
if not path.exists():
|
|
81
|
+
raise FileNotFoundError(
|
|
82
|
+
f"Estimator script not found: {script_path}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Dynamic import of the user script
|
|
86
|
+
spec = importlib.util.spec_from_file_location("user_estimator", path)
|
|
87
|
+
if spec is None:
|
|
88
|
+
raise ImportError(f"Cannot load script: {script_path}")
|
|
89
|
+
|
|
90
|
+
module = importlib.util.module_from_spec(spec)
|
|
91
|
+
sys.modules["user_estimator"] = module
|
|
92
|
+
if spec.loader is None:
|
|
93
|
+
raise ImportError(f"Cannot find loader for: {script_path}")
|
|
94
|
+
spec.loader.exec_module(module)
|
|
95
|
+
|
|
96
|
+
func_name = self.config.get("function_name", "estimate")
|
|
97
|
+
if not hasattr(module, func_name):
|
|
98
|
+
raise AttributeError(
|
|
99
|
+
f"Function '{func_name}' not found in {script_path}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self._estimator_func = getattr(module, func_name)
|
|
103
|
+
|
|
104
|
+
def estimate(self, model_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
105
|
+
"""
|
|
106
|
+
Estimate performance using the user-provided hardware script.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
model_info: ModelInfo dictionary.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Dictionary with performance metrics.
|
|
113
|
+
"""
|
|
114
|
+
# Get software metrics as fallback
|
|
115
|
+
macs = calculate_macs(model_info)
|
|
116
|
+
params = calculate_params(model_info)
|
|
117
|
+
peak_ram = calculate_peak_ram(model_info)
|
|
118
|
+
|
|
119
|
+
if self._estimator_func is None:
|
|
120
|
+
self._load_estimator()
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# Call the user's estimate function
|
|
124
|
+
result = self._estimator_func(model_info)
|
|
125
|
+
|
|
126
|
+
# Validate the result has all required fields
|
|
127
|
+
required_keys = ["score", "macs", "params", "peak_ram", "flash"]
|
|
128
|
+
for key in required_keys:
|
|
129
|
+
if key not in result:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Estimator result missing required key: {key}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# If latency_ms is not provided, estimate it
|
|
135
|
+
if "latency_ms" not in result:
|
|
136
|
+
clock = self.config.get("clock_speed", 100000000)
|
|
137
|
+
result["latency_ms"] = (macs / clock) * 1000.0
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
# Fallback to software estimation on error
|
|
143
|
+
max_macs = self.config["max_macs"]
|
|
144
|
+
max_params = self.config["max_params"]
|
|
145
|
+
max_ram = self.config["max_ram"]
|
|
146
|
+
|
|
147
|
+
macs_score = (
|
|
148
|
+
1.0 - min(macs / max_macs, 1.0) if max_macs > 0 else 0.0
|
|
149
|
+
)
|
|
150
|
+
params_score = (
|
|
151
|
+
1.0 - min(params / max_params, 1.0) if max_params > 0 else 0.0
|
|
152
|
+
)
|
|
153
|
+
ram_score = (
|
|
154
|
+
1.0 - min(peak_ram / max_ram, 1.0) if max_ram > 0 else 0.0
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
score = (
|
|
158
|
+
0.4 * macs_score + 0.3 * params_score + 0.3 * ram_score
|
|
159
|
+
) * 100.0
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
"score": score,
|
|
163
|
+
"macs": macs,
|
|
164
|
+
"params": params,
|
|
165
|
+
"peak_ram": peak_ram,
|
|
166
|
+
"flash": params + 1024,
|
|
167
|
+
"latency_ms": (macs / 100000000) * 1000.0,
|
|
168
|
+
"details": {
|
|
169
|
+
"estimator": "hardware_hal",
|
|
170
|
+
"fallback": True,
|
|
171
|
+
"error": str(e),
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
def get_info(self) -> Dict[str, str]:
|
|
176
|
+
"""Get estimator information."""
|
|
177
|
+
return {
|
|
178
|
+
"name": "HardwareHALEstimator",
|
|
179
|
+
"version": "1.0",
|
|
180
|
+
"type": "closed_loop",
|
|
181
|
+
"description": "Hardware HAL estimator with user script",
|
|
182
|
+
"script_path": str(self.config.get("script_path", "")),
|
|
183
|
+
"function_name": self.config.get("function_name", "estimate"),
|
|
184
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
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
|
+
# QEMU-based estimator using icount mode for deterministic instruction counts.
|
|
21
|
+
|
|
22
|
+
import subprocess
|
|
23
|
+
import tempfile
|
|
24
|
+
import re
|
|
25
|
+
from typing import Dict, Any, Optional
|
|
26
|
+
|
|
27
|
+
from TinyMLC.ANG.estimator import Estimator
|
|
28
|
+
from TinyMLC.ANG.utils import (calculate_macs, calculate_params,
|
|
29
|
+
calculate_peak_ram)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class QemuEstimator(Estimator):
|
|
33
|
+
"""
|
|
34
|
+
QEMU-based estimator (closed-loop).
|
|
35
|
+
|
|
36
|
+
This estimator compiles the model to an ELF file and runs it under
|
|
37
|
+
QEMU with icount mode enabled. The instruction count is used as
|
|
38
|
+
a stable, deterministic performance metric.
|
|
39
|
+
|
|
40
|
+
This is a "closed-loop" estimator because it provides feedback
|
|
41
|
+
from actual execution (even if simulated).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
45
|
+
"""
|
|
46
|
+
Initialize the QEMU estimator.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
config: Configuration with QEMU and compilation settings.
|
|
50
|
+
"""
|
|
51
|
+
super().__init__(config)
|
|
52
|
+
|
|
53
|
+
self.default_config = {
|
|
54
|
+
"qemu_binary": "qemu-system-arm", # QEMU binary to use
|
|
55
|
+
"cpu": "cortex-m4", # CPU model
|
|
56
|
+
"icount_shift": 0, # icount shift (0 = 1 instr/tick)
|
|
57
|
+
"clock_speed": 100000000, # Clock speed in Hz
|
|
58
|
+
"timeout": 30, # Timeout in seconds
|
|
59
|
+
"max_macs": 100000, # For score calculation
|
|
60
|
+
"max_params": 50000,
|
|
61
|
+
"max_ram": 32768,
|
|
62
|
+
"gcc_binary": "arm-none-eabi-gcc", # Cross-compiler
|
|
63
|
+
"linker_script": "mcu.ld", # Linker script path
|
|
64
|
+
}
|
|
65
|
+
self.config = {**self.default_config, **(config or {})}
|
|
66
|
+
self._compile_cache = {}
|
|
67
|
+
|
|
68
|
+
def estimate(self, model_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
69
|
+
"""
|
|
70
|
+
Estimate performance by running the model under QEMU.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
model_info: ModelInfo dictionary.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dictionary with performance metrics.
|
|
77
|
+
"""
|
|
78
|
+
# Get software metrics first
|
|
79
|
+
macs = calculate_macs(model_info)
|
|
80
|
+
params = calculate_params(model_info)
|
|
81
|
+
peak_ram = calculate_peak_ram(model_info)
|
|
82
|
+
|
|
83
|
+
# Compile and run under QEMU
|
|
84
|
+
try:
|
|
85
|
+
elf_path = self._compile_model(model_info)
|
|
86
|
+
instr_count = self._run_qemu_icount(elf_path)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
# On failure, fall back to software estimates
|
|
89
|
+
instr_count = macs # Approximate instruction count
|
|
90
|
+
|
|
91
|
+
# Estimate latency from instruction count
|
|
92
|
+
latency_ms = (instr_count / self.config["clock_speed"]) * 1000.0
|
|
93
|
+
|
|
94
|
+
# Flash usage from ELF size
|
|
95
|
+
flash = (
|
|
96
|
+
self._get_elf_flash_size(elf_path)
|
|
97
|
+
if elf_path
|
|
98
|
+
else params + 1024
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Calculate score (same as software estimator)
|
|
102
|
+
max_macs = self.config["max_macs"]
|
|
103
|
+
max_params = self.config["max_params"]
|
|
104
|
+
max_ram = self.config["max_ram"]
|
|
105
|
+
|
|
106
|
+
macs_score = 1.0 - min(macs / max_macs, 1.0) if max_macs > 0 else 0.0
|
|
107
|
+
params_score = (
|
|
108
|
+
1.0 - min(params / max_params, 1.0) if max_params > 0 else 0.0
|
|
109
|
+
)
|
|
110
|
+
ram_score = 1.0 - min(peak_ram / max_ram, 1.0) if max_ram > 0 else 0.0
|
|
111
|
+
|
|
112
|
+
score = (
|
|
113
|
+
0.4 * macs_score + 0.3 * params_score + 0.3 * ram_score
|
|
114
|
+
) * 100.0
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"score": score,
|
|
118
|
+
"macs": macs,
|
|
119
|
+
"params": params,
|
|
120
|
+
"peak_ram": peak_ram,
|
|
121
|
+
"flash": flash,
|
|
122
|
+
"latency_ms": latency_ms,
|
|
123
|
+
"instr_count": instr_count, # QEMU-specific
|
|
124
|
+
"details": {
|
|
125
|
+
"estimator": "qemu",
|
|
126
|
+
"qemu_cpu": self.config["cpu"],
|
|
127
|
+
"icount_shift": self.config["icount_shift"],
|
|
128
|
+
"elf_path": elf_path,
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
def get_info(self) -> Dict[str, str]:
|
|
133
|
+
"""Get estimator information."""
|
|
134
|
+
return {
|
|
135
|
+
"name": "QemuEstimator",
|
|
136
|
+
"version": "1.0",
|
|
137
|
+
"type": "closed_loop",
|
|
138
|
+
"description": "QEMU icount-based estimator",
|
|
139
|
+
"qemu_binary": self.config["qemu_binary"],
|
|
140
|
+
"cpu": self.config["cpu"],
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def _compile_model(self, model_info: Dict[str, Any]) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Compile the model to an ELF file.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
model_info: ModelInfo dictionary.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Path to the compiled ELF file.
|
|
152
|
+
"""
|
|
153
|
+
# This would call the TinyMLC code generator
|
|
154
|
+
# For now, we return a placeholder
|
|
155
|
+
# In production, this would:
|
|
156
|
+
# 1. Call generate_c_code(model_info)
|
|
157
|
+
# 2. Compile with the cross-compiler
|
|
158
|
+
# 3. Return the ELF path
|
|
159
|
+
|
|
160
|
+
# Placeholder implementation
|
|
161
|
+
with tempfile.NamedTemporaryFile(suffix=".elf", delete=False) as f:
|
|
162
|
+
elf_path = f.name
|
|
163
|
+
|
|
164
|
+
# TODO: Actually compile the model
|
|
165
|
+
# This requires integration with TinyMLC code generation
|
|
166
|
+
|
|
167
|
+
return elf_path
|
|
168
|
+
|
|
169
|
+
def _run_qemu_icount(self, elf_path: str) -> int:
|
|
170
|
+
"""
|
|
171
|
+
Run the ELF under QEMU with icount mode.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
elf_path: Path to the ELF file.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Instruction count.
|
|
178
|
+
"""
|
|
179
|
+
cmd = [
|
|
180
|
+
self.config["qemu_binary"],
|
|
181
|
+
"-cpu",
|
|
182
|
+
self.config["cpu"],
|
|
183
|
+
"-nographic",
|
|
184
|
+
"-icount",
|
|
185
|
+
f"shift={self.config['icount_shift']}",
|
|
186
|
+
"-semihosting",
|
|
187
|
+
"-semihosting-config",
|
|
188
|
+
"enable=on,target=native",
|
|
189
|
+
"-kernel",
|
|
190
|
+
elf_path,
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
result = subprocess.run(
|
|
195
|
+
cmd,
|
|
196
|
+
capture_output=True,
|
|
197
|
+
text=True,
|
|
198
|
+
timeout=self.config["timeout"],
|
|
199
|
+
)
|
|
200
|
+
# Parse instruction count from output
|
|
201
|
+
return self._parse_icount(result.stdout + result.stderr)
|
|
202
|
+
except subprocess.TimeoutExpired:
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
def _parse_icount(self, output: str) -> int:
|
|
206
|
+
"""
|
|
207
|
+
Parse instruction count from QEMU output.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
output: QEMU stdout/stderr.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Instruction count.
|
|
214
|
+
"""
|
|
215
|
+
# Try different patterns
|
|
216
|
+
patterns = [
|
|
217
|
+
r"INSTR_COUNT:\s*(\d+)",
|
|
218
|
+
r"icount:\s*(\d+)",
|
|
219
|
+
r"instructions\s*:\s*(\d+)",
|
|
220
|
+
]
|
|
221
|
+
for pattern in patterns:
|
|
222
|
+
match = re.search(pattern, output, re.IGNORECASE)
|
|
223
|
+
if match:
|
|
224
|
+
return int(match.group(1))
|
|
225
|
+
|
|
226
|
+
return 0
|
|
227
|
+
|
|
228
|
+
def _get_elf_flash_size(self, elf_path: str) -> int:
|
|
229
|
+
"""
|
|
230
|
+
Get flash size from ELF using size command.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
elf_path: Path to the ELF file.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Flash size in bytes.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
# Use GNU size to get text + data
|
|
240
|
+
size_cmd = self.config.get("size_binary", "arm-none-eabi-size")
|
|
241
|
+
result = subprocess.run(
|
|
242
|
+
[size_cmd, elf_path],
|
|
243
|
+
capture_output=True,
|
|
244
|
+
text=True,
|
|
245
|
+
)
|
|
246
|
+
if result.returncode == 0:
|
|
247
|
+
lines = result.stdout.strip().split("\n")
|
|
248
|
+
if len(lines) >= 2:
|
|
249
|
+
parts = lines[1].split()
|
|
250
|
+
if len(parts) >= 3:
|
|
251
|
+
# text, data, bss
|
|
252
|
+
text = int(parts[0])
|
|
253
|
+
data = int(parts[1])
|
|
254
|
+
return text + data
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
return 0
|