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.
Files changed (47) hide show
  1. TinyMLC/ANG/__init__.py +0 -0
  2. TinyMLC/ANG/args.py +86 -0
  3. TinyMLC/ANG/estimator.py +103 -0
  4. TinyMLC/ANG/estimator_hal.py +184 -0
  5. TinyMLC/ANG/estimator_qemu.py +257 -0
  6. TinyMLC/ANG/estimator_software.py +130 -0
  7. TinyMLC/ANG/model_builder.py +508 -0
  8. TinyMLC/ANG/model_generator.py +439 -0
  9. TinyMLC/ANG/model_info.py +283 -0
  10. TinyMLC/ANG/utils.py +420 -0
  11. TinyMLC/__init__.py +0 -0
  12. TinyMLC/cli.py +126 -0
  13. TinyMLC/codegen.py +877 -0
  14. TinyMLC/converter/__init__.py +0 -0
  15. TinyMLC/converter/export_weights.py +382 -0
  16. TinyMLC/converter/parser_litert.py +757 -0
  17. TinyMLC/converter/parser_onnx.py +649 -0
  18. TinyMLC/generate_lut.py +97 -0
  19. TinyMLC/handlers.py +325 -0
  20. TinyMLC/ops.py +76 -0
  21. TinyMLC/templates/lut.c.tpl +23 -0
  22. TinyMLC/templates/lut.h.tpl +67 -0
  23. TinyMLC/templates/model.c.tpl +314 -0
  24. TinyMLC/templates/model.h.tpl +66 -0
  25. TinyMLC/transform/__init__.py +0 -0
  26. TinyMLC/transform/algebraic.py +286 -0
  27. TinyMLC/transform/base.py +58 -0
  28. TinyMLC/transform/constant_folding.py +260 -0
  29. TinyMLC/transform/cse.py +192 -0
  30. TinyMLC/transform/dce.py +182 -0
  31. TinyMLC/transform/fusion.py +723 -0
  32. TinyMLC/transform/memory.py +200 -0
  33. TinyMLC/transform/pass_manager.py +101 -0
  34. TinyMLC/transform/simplify.py +515 -0
  35. tinymlc-0.1.0.dist-info/METADATA +49 -0
  36. tinymlc-0.1.0.dist-info/RECORD +47 -0
  37. tinymlc-0.1.0.dist-info/WHEEL +4 -0
  38. tinymlc-0.1.0.dist-info/entry_points.txt +2 -0
  39. tinymlc-0.1.0.dist-info/licenses/LICENSE +201 -0
  40. utils/__init__.py +0 -0
  41. utils/arm-none-eabi-gcc.cmake +53 -0
  42. utils/dump.py +86 -0
  43. utils/generate_onnx_models.py +183 -0
  44. utils/generate_tflite_models.py +236 -0
  45. utils/pack_macos.sh +88 -0
  46. utils/path.py +31 -0
  47. utils/riscv-none-elf-gcc.cmake +50 -0
@@ -0,0 +1,439 @@
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
+ # Network structure generation using random and genetic algorithms.
21
+
22
+ import copy
23
+ import json
24
+ import random
25
+ import sys
26
+
27
+ from typing import Dict, Any, Optional
28
+
29
+ from TinyMLC.ANG.model_builder import ModelBuilder
30
+ from TinyMLC.ANG.estimator import Estimator
31
+ from utils.dump import fatal_error, info
32
+
33
+
34
+ class ModelGenerator:
35
+ """
36
+ Model structure generator.
37
+
38
+ This class handles:
39
+ - Random: Generate random networks and pick the best.
40
+ - Genetic algorithm: Iteratively evolve a population of networks.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ estimator: Estimator,
46
+ config: Optional[Dict[str, Any]] = None,
47
+ ):
48
+ self.estimator = estimator
49
+ self.config = config or {}
50
+
51
+ self.default_config = {
52
+ # params
53
+ "population_size": 50,
54
+ "generations": 50,
55
+ "mutation_rate": 0.1,
56
+ "crossover_rate": 0.8,
57
+ "tournament_size": 3,
58
+ "early_stop": 10,
59
+ # Network structure params
60
+ "max_layers": 10,
61
+ "min_layers": 3,
62
+ "channels_options": [8, 16, 32, 64],
63
+ "kernel_options": [1, 3, 5],
64
+ "stride_options": [1, 2],
65
+ "fc_units_options": [32, 64, 128],
66
+ "upsample_factors": [2, 4],
67
+ # Task config
68
+ "task_type": "classification",
69
+ # classification / detection / segmentation
70
+ "input_shape": [1, 28, 28, 1],
71
+ "output_shape": [1, 10],
72
+ # Constraints (for scoring)
73
+ "max_macs": 100000,
74
+ "max_ram": 30 * 1024,
75
+ "max_flash": 64 * 1024,
76
+ }
77
+ self.config = {**self.default_config, **(config or {})}
78
+ self._best_score = -1.0
79
+ self._best_structure = None
80
+
81
+ def _is_image_input(self) -> bool:
82
+ """Check if input is image-like (4D: NHWC)."""
83
+ shape = self.config["input_shape"]
84
+ return len(shape) == 4
85
+
86
+ def _is_1d_input(self) -> bool:
87
+ """Check if input is 1D (sensor, audio, etc)."""
88
+ shape = self.config["input_shape"]
89
+ return len(shape) == 2 or (len(shape) == 3 and shape[0] == 1)
90
+
91
+ def generate_random(self, num_samples: int = 100) -> Dict[str, Any]:
92
+ """Random."""
93
+ best_structure = None
94
+ best_score = -1.0
95
+
96
+ for _ in range(num_samples):
97
+ structure = self._generate_random_structure()
98
+ model_info = self._structure_to_model_info(structure)
99
+ result = self.estimator.estimate(model_info)
100
+ score = result.get("score", 0.0)
101
+
102
+ if score > best_score:
103
+ best_score = score
104
+ best_structure = structure
105
+
106
+ self._best_score = best_score
107
+ self._best_structure = best_structure
108
+ return best_structure
109
+
110
+ def generate_evolved(self) -> Dict[str, Any]:
111
+ """Genetic algorithm."""
112
+ population_size = self.config["population_size"]
113
+ population = [
114
+ self._generate_random_structure()
115
+ for _ in range(population_size)
116
+ ]
117
+
118
+ best_structure = None
119
+ best_score = -1.0
120
+ no_improvement_count = 0
121
+ early_stop = self.config["early_stop"]
122
+
123
+ for generation in range(self.config["generations"]):
124
+ scores = []
125
+ for structure in population:
126
+ model_info = self._structure_to_model_info(structure)
127
+ result = self.estimator.estimate(model_info)
128
+ scores.append(result.get("score", 0.0))
129
+
130
+ gen_best_score = max(scores)
131
+ gen_best_idx = scores.index(gen_best_score)
132
+
133
+ if gen_best_score > best_score:
134
+ best_score = gen_best_score
135
+ best_structure = copy.deepcopy(population[gen_best_idx])
136
+ no_improvement_count = 0
137
+ else:
138
+ no_improvement_count += 1
139
+
140
+ if no_improvement_count >= early_stop:
141
+ break
142
+
143
+ selected = self._tournament_selection(population, scores)
144
+
145
+ next_population = []
146
+ for i in range(0, population_size, 2):
147
+ parent1 = selected[i % len(selected)]
148
+ parent2 = selected[(i + 1) % len(selected)]
149
+
150
+ if random.random() < self.config["crossover_rate"]:
151
+ child1, child2 = self._crossover(parent1, parent2)
152
+ else:
153
+ child1 = copy.deepcopy(parent1)
154
+ child2 = copy.deepcopy(parent2)
155
+
156
+ child1 = self._mutate(child1)
157
+ child2 = self._mutate(child2)
158
+
159
+ next_population.append(child1)
160
+ if len(next_population) < population_size:
161
+ next_population.append(child2)
162
+
163
+ population = next_population
164
+ best_model_info = self._structure_to_model_info(best_structure)
165
+ print(f"MODEL_INFO: {json.dumps(best_model_info)}")
166
+ sys.stdout.flush()
167
+
168
+ self._best_structure = best_structure
169
+ return best_structure
170
+
171
+ def _generate_random_structure(self) -> Dict[str, Any]:
172
+ """Generate a random network structure based on task type."""
173
+ task_type = self.config["task_type"]
174
+ max_layers = self.config["max_layers"]
175
+ min_layers = self.config["min_layers"]
176
+ num_layers = random.randint(min_layers, max_layers)
177
+
178
+ layers = []
179
+ input_shape = self.config["input_shape"]
180
+ output_shape = self.config["output_shape"]
181
+
182
+ for i in range(num_layers):
183
+ # Last layer depends on task type
184
+ if i == num_layers - 1:
185
+ if task_type == "classification":
186
+ layer_type = "fc"
187
+ elif task_type == "detection":
188
+ layer_type = "detection_head"
189
+ else: # segmentation
190
+ layer_type = "conv" # final conv for pixel-wise output
191
+ elif i == 0:
192
+ # First layer is always conv
193
+ layer_type = "conv"
194
+ else:
195
+ # Middle layers: mix of conv, pool
196
+ if task_type == "segmentation":
197
+ # Segmentation needs upsample in decoder
198
+ # Keep simple with conv only (pool not implemented)
199
+ layer_type = "conv"
200
+ else:
201
+ layer_type = "conv"
202
+ # TODO: Re-enable pool after implementing builder.add_pool()
203
+ # layer_type = random.choice(["conv", "pool"])
204
+
205
+ if layer_type == "conv":
206
+ channels = random.choice(self.config["channels_options"])
207
+ kernel = random.choice(self.config["kernel_options"])
208
+ stride = random.choice(self.config["stride_options"])
209
+ layers.append({
210
+ "type": "conv",
211
+ "channels": channels,
212
+ "kernel": kernel,
213
+ "stride": stride,
214
+ })
215
+ elif layer_type == "pool":
216
+ kernel = random.choice([2, 3])
217
+ stride = random.choice([2, 3])
218
+ layers.append({
219
+ "type": "pool",
220
+ "kernel": kernel,
221
+ "stride": stride,
222
+ })
223
+ elif layer_type == "fc":
224
+ units = random.choice(self.config["fc_units_options"])
225
+ layers.append({
226
+ "type": "fc",
227
+ "units": units,
228
+ })
229
+ elif layer_type == "detection_head":
230
+ # Detection head: a few conv layers + output
231
+ layers.append({
232
+ "type": "detection_head",
233
+ "num_anchors": random.choice([3, 4, 5]),
234
+ })
235
+
236
+ return {
237
+ "layers": layers,
238
+ "input_shape": input_shape,
239
+ "output_shape": output_shape,
240
+ }
241
+
242
+ def _structure_to_model_info(
243
+ self, structure: Dict[str, Any]
244
+ ) -> Dict[str, Any]:
245
+ """Convert structure to model_info without weights."""
246
+ builder = ModelBuilder("ang_generated")
247
+
248
+ input_shape = structure["input_shape"]
249
+ output_shape = structure["output_shape"]
250
+
251
+ input_idx = builder.add_input("input", input_shape, "int8")
252
+
253
+ current_idx = input_idx
254
+ current_shape = list(input_shape)
255
+
256
+ for layer in structure["layers"]:
257
+ layer_type = layer["type"]
258
+
259
+ if layer_type == "conv":
260
+ kernel = layer.get("kernel", 3)
261
+ channels = layer.get("channels", 16)
262
+ stride = layer.get("stride", 1)
263
+
264
+ # Build output shape based on input dimensions
265
+ if len(current_shape) == 4: # NHWC image
266
+ output_shape_layer = list(current_shape)
267
+ output_shape_layer[-1] = channels
268
+ elif len(current_shape) == 3: # 1D with channel
269
+ output_shape_layer = list(current_shape)
270
+ output_shape_layer[-1] = channels
271
+ else:
272
+ # Fallback: just append channels
273
+ output_shape_layer = current_shape + [channels]
274
+
275
+ output_idx = builder.add_tensor("conv_out",
276
+ output_shape_layer, "int8")
277
+ builder.add_conv(current_idx, output_idx,
278
+ kernel, channels, stride)
279
+ current_idx = output_idx
280
+ current_shape = output_shape_layer
281
+
282
+ elif layer_type == "pool":
283
+ kernel = layer.get("kernel", 2)
284
+ stride = layer.get("stride", 2)
285
+
286
+ # Pool reduces spatial dimensions
287
+ if len(current_shape) == 4: # NHWC
288
+ output_shape_layer = list(current_shape)
289
+ stride_div = max(1, output_shape_layer[1] // stride)
290
+ output_shape_layer[1] = stride_div
291
+ stride_div = max(1, output_shape_layer[2] // stride)
292
+ output_shape_layer[2] = stride_div
293
+ elif len(current_shape) == 3:
294
+ output_shape_layer = list(current_shape)
295
+ stride_div = max(1, output_shape_layer[1] // stride)
296
+ output_shape_layer[1] = stride_div
297
+ else:
298
+ output_shape_layer = current_shape
299
+
300
+ output_idx = builder.add_tensor(
301
+ "pool_out", output_shape_layer, "int8"
302
+ )
303
+ # TODO: Add pool op to builder
304
+ current_idx = output_idx
305
+ current_shape = output_shape_layer
306
+
307
+ elif layer_type == "fc":
308
+ units = layer.get("units", 64)
309
+ output_idx = builder.add_tensor("fc_out", [1, units], "int8")
310
+ builder.add_fc(current_idx, output_idx, units)
311
+ current_idx = output_idx
312
+ current_shape = [1, units]
313
+
314
+ elif layer_type == "detection_head":
315
+ # Detection head: placeholder
316
+ num_anchors = layer.get("num_anchors", 3)
317
+ num_classes = output_shape[-1] if len(output_shape) > 1 else 10
318
+ # Output boxes: [N, num_anchors, 4]
319
+ # Output classes: [N, num_anchors, num_classes]
320
+ boxes_idx = builder.add_tensor(
321
+ "detection_boxes", [1, num_anchors, 4], "int8"
322
+ )
323
+ classes_idx = builder.add_tensor(
324
+ "detection_classes", [1, num_anchors, num_classes], "int8"
325
+ )
326
+ builder.add_detection_head(
327
+ current_idx, boxes_idx, classes_idx,
328
+ num_anchors=num_anchors, num_classes=num_classes
329
+ )
330
+ current_idx = boxes_idx # Just use boxes as output ref
331
+ current_shape = [1, num_anchors, 4]
332
+
333
+ # Final output
334
+ output_idx = builder.add_output("output", output_shape, "int8")
335
+ if current_idx != output_idx:
336
+ # If current shape doesn't match output_shape, add a final FC
337
+ flat_size = 1
338
+ for d in current_shape:
339
+ flat_size *= d
340
+ builder.add_fc(current_idx, output_idx, output_shape[-1], "none")
341
+
342
+ model_info = builder.build()
343
+ return model_info.to_dict()
344
+
345
+ def _tournament_selection(self, population, scores):
346
+ """Tournament selection."""
347
+ selected = []
348
+ tournament_size = self.config["tournament_size"]
349
+
350
+ for _ in range(len(population)):
351
+ indices = random.sample(range(len(population)), tournament_size)
352
+ best_idx = max(indices, key=lambda i: scores[i])
353
+ selected.append(population[best_idx])
354
+
355
+ return selected
356
+
357
+ def _crossover(self, parent1, parent2):
358
+ """Single-point crossover."""
359
+ layers1 = parent1["layers"]
360
+ layers2 = parent2["layers"]
361
+
362
+ if len(layers1) < 2 or len(layers2) < 2:
363
+ return copy.deepcopy(parent1), copy.deepcopy(parent2)
364
+
365
+ pos1 = random.randint(1, len(layers1) - 1)
366
+ pos2 = random.randint(1, len(layers2) - 1)
367
+
368
+ child1_layers = layers1[:pos1] + layers2[pos2:]
369
+ child2_layers = layers2[:pos2] + layers1[pos1:]
370
+
371
+ child1 = copy.deepcopy(parent1)
372
+ child2 = copy.deepcopy(parent2)
373
+ child1["layers"] = child1_layers
374
+ child2["layers"] = child2_layers
375
+
376
+ return child1, child2
377
+
378
+ def _mutate(self, structure):
379
+ """Mutate a structure."""
380
+ layers = structure["layers"]
381
+
382
+ if not layers:
383
+ return structure
384
+
385
+ mutation_rate = self.config["mutation_rate"]
386
+
387
+ # Get options from config
388
+ channels_opts = self.config.get("channels_options", [8, 16, 32, 64])
389
+ kernel_opts = self.config.get("kernel_options", [1, 3, 5])
390
+ stride_opts = self.config.get("stride_options", [1, 2])
391
+ fc_units_opts = self.config.get("fc_units_options", [32, 64, 128])
392
+
393
+ for i, layer in enumerate(layers):
394
+ if random.random() < mutation_rate:
395
+ layer_type = layer["type"]
396
+ if layer_type == "conv":
397
+ layer["channels"] = random.choice(channels_opts)
398
+ layer["kernel"] = random.choice(kernel_opts)
399
+ layer["stride"] = random.choice(stride_opts)
400
+ elif layer_type == "pool":
401
+ layer["kernel"] = random.choice([2, 3])
402
+ layer["stride"] = random.choice([2, 3])
403
+ elif layer_type == "fc":
404
+ layer["units"] = random.choice(fc_units_opts)
405
+ elif layer_type == "detection_head":
406
+ layer["num_anchors"] = random.choice([3, 4, 5])
407
+
408
+ return structure
409
+
410
+ def generate(self, mode: str = "genetic") -> Dict[str, Any]:
411
+ """
412
+ Unified entry point for network generation.
413
+
414
+ This is the main method called by CLI.
415
+ It builds model_info, and fills random weights.
416
+
417
+ Args:
418
+ mode: "random" or "genetic"
419
+
420
+ Returns:
421
+ Complete model_info dict with random weights.
422
+ """
423
+ task_type = self.config.get("task_type", "classification")
424
+ info(f"Generating {task_type} network (mode={mode})")
425
+
426
+ if mode == "random":
427
+ num_samples = self.config.get("num_samples", 100)
428
+ structure = self.generate_random(num_samples)
429
+ elif mode == "genetic":
430
+ structure = self.generate_evolved()
431
+ else:
432
+ fatal_error(f"Unknown generation mode: {mode}")
433
+
434
+ # Convert structure to model_info
435
+ model_info = self._structure_to_model_info(structure)
436
+
437
+ info(f"Generated network: {len(structure['layers'])} layers")
438
+
439
+ return model_info