tan-cli 0.7.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 (330) hide show
  1. tan/__init__.py +0 -0
  2. tan/__main__.py +120 -0
  3. tan/cli.py +939 -0
  4. tan/command_table.py +265 -0
  5. tan/commands/__init__.py +8 -0
  6. tan/commands/bootstrap_cmd.py +4856 -0
  7. tan/commands/bootstrap_patches.py +164 -0
  8. tan/commands/bootstrap_toolchain_fetch.py +155 -0
  9. tan/commands/bootstrap_toolchain_pin.py +36 -0
  10. tan/commands/build/__init__.py +1 -0
  11. tan/commands/build/configure_inputs.py +277 -0
  12. tan/commands/build/execute.py +1852 -0
  13. tan/commands/build/host_python.py +59 -0
  14. tan/commands/build/link_stale.py +78 -0
  15. tan/commands/build/manifest.py +428 -0
  16. tan/commands/build/materialise.py +81 -0
  17. tan/commands/build/token_substitution.py +381 -0
  18. tan/commands/build/toolchain.py +482 -0
  19. tan/commands/build/user_defines_wipe.py +115 -0
  20. tan/commands/build/workspace_patches.py +65 -0
  21. tan/commands/build_cmd.py +2236 -0
  22. tan/commands/build_output.py +331 -0
  23. tan/commands/clean_cmd.py +1183 -0
  24. tan/commands/completion_cmd.py +786 -0
  25. tan/commands/debug_config_cmd.py +2425 -0
  26. tan/commands/diff_cmd.py +940 -0
  27. tan/commands/doctor_cmd.py +4767 -0
  28. tan/commands/examples_cmd.py +644 -0
  29. tan/commands/explain_cmd.py +1319 -0
  30. tan/commands/faultdecode_cmd.py +931 -0
  31. tan/commands/flash_atoc_guard.py +240 -0
  32. tan/commands/flash_cmd.py +5731 -0
  33. tan/commands/flash_mram_guard.py +150 -0
  34. tan/commands/flash_ram.py +578 -0
  35. tan/commands/flash_raw.py +583 -0
  36. tan/commands/generate_cmd.py +1456 -0
  37. tan/commands/image_cmd.py +753 -0
  38. tan/commands/init_cmd.py +1740 -0
  39. tan/commands/inspect_cmd.py +377 -0
  40. tan/commands/kconfig_cmd.py +734 -0
  41. tan/commands/model_cmd.py +1514 -0
  42. tan/commands/model_device_cmd.py +351 -0
  43. tan/commands/model_device_live.py +234 -0
  44. tan/commands/model_host_cmd.py +430 -0
  45. tan/commands/model_zoo_cmd.py +363 -0
  46. tan/commands/monitor_actions.py +125 -0
  47. tan/commands/monitor_cmd.py +626 -0
  48. tan/commands/monitor_session.py +505 -0
  49. tan/commands/new_som_cmd.py +1522 -0
  50. tan/commands/pinmux_cmd.py +568 -0
  51. tan/commands/presets_cmd.py +1123 -0
  52. tan/commands/probe_cmd.py +421 -0
  53. tan/commands/reset_cmd.py +354 -0
  54. tan/commands/reset_confirm.py +168 -0
  55. tan/commands/run_cmd.py +637 -0
  56. tan/commands/scaffold_cmd.py +490 -0
  57. tan/commands/sdk_cmd.py +1723 -0
  58. tan/commands/size_cmd.py +1020 -0
  59. tan/commands/support_bundle_cmd.py +1073 -0
  60. tan/commands/trace_cmd.py +341 -0
  61. tan/commands/validate_cmd.py +2009 -0
  62. tan/commands/west_forward_cmd.py +736 -0
  63. tan/commands/workspace_patch_check.py +296 -0
  64. tan/core/__init__.py +1 -0
  65. tan/core/alp_conf_pregen.py +100 -0
  66. tan/core/artifact_provenance.py +240 -0
  67. tan/core/atoc_guard.py +276 -0
  68. tan/core/atoc_guard_messages.py +232 -0
  69. tan/core/atoc_replacement.py +118 -0
  70. tan/core/atomic_write.py +178 -0
  71. tan/core/board_context.py +351 -0
  72. tan/core/board_diagnostic.py +111 -0
  73. tan/core/board_files.py +221 -0
  74. tan/core/board_validator.py +542 -0
  75. tan/core/board_validator_compat.py +196 -0
  76. tan/core/board_validator_run.py +129 -0
  77. tan/core/board_validator_schema.py +186 -0
  78. tan/core/board_validator_skew.py +46 -0
  79. tan/core/board_yaml_edit.py +295 -0
  80. tan/core/board_yaml_pos.py +118 -0
  81. tan/core/bootstrap.py +2572 -0
  82. tan/core/build_plan.py +625 -0
  83. tan/core/consent.py +104 -0
  84. tan/core/console_filter.py +245 -0
  85. tan/core/debug_launch.py +1803 -0
  86. tan/core/devicetree_lint.py +168 -0
  87. tan/core/dir_removal.py +229 -0
  88. tan/core/doctor_git.py +172 -0
  89. tan/core/doctor_libraries.py +253 -0
  90. tan/core/doctor_render.py +185 -0
  91. tan/core/doctor_scope.py +66 -0
  92. tan/core/doctor_setools.py +237 -0
  93. tan/core/doctor_stale.py +180 -0
  94. tan/core/document_guards.py +723 -0
  95. tan/core/dp_id.py +97 -0
  96. tan/core/elf_load.py +63 -0
  97. tan/core/error_catalog.py +171 -0
  98. tan/core/example_catalog.py +499 -0
  99. tan/core/example_facets.py +151 -0
  100. tan/core/faultdecode.py +521 -0
  101. tan/core/flash_plan.py +2501 -0
  102. tan/core/flow_d_report.py +381 -0
  103. tan/core/flow_d_uptime.py +164 -0
  104. tan/core/fs_confine.py +130 -0
  105. tan/core/global_flags.py +230 -0
  106. tan/core/host_python.py +210 -0
  107. tan/core/image_bundle.py +247 -0
  108. tan/core/inert.py +147 -0
  109. tan/core/jlink_binary.py +128 -0
  110. tan/core/jlink_probe.py +481 -0
  111. tan/core/jsonc_splice.py +404 -0
  112. tan/core/launch_provenance.py +257 -0
  113. tan/core/link_refusal.py +76 -0
  114. tan/core/metadata_schema.py +376 -0
  115. tan/core/model_check.py +227 -0
  116. tan/core/model_device.py +449 -0
  117. tan/core/model_doctor.py +198 -0
  118. tan/core/model_host.py +48 -0
  119. tan/core/model_list.py +112 -0
  120. tan/core/model_zoo.py +353 -0
  121. tan/core/module_template.py +340 -0
  122. tan/core/mram_link.py +87 -0
  123. tan/core/new_som_ospi.py +94 -0
  124. tan/core/os_class.py +159 -0
  125. tan/core/patch_paths.py +87 -0
  126. tan/core/pending.py +40 -0
  127. tan/core/plain_zephyr_plan.py +138 -0
  128. tan/core/plan_exec.py +373 -0
  129. tan/core/plan_tokens.py +600 -0
  130. tan/core/probe.py +155 -0
  131. tan/core/probe_plan.py +237 -0
  132. tan/core/proxy.py +168 -0
  133. tan/core/publish.py +31 -0
  134. tan/core/python_floor.py +178 -0
  135. tan/core/ram_run.py +694 -0
  136. tan/core/ram_watch.py +231 -0
  137. tan/core/raw_write.py +160 -0
  138. tan/core/reset_plan.py +56 -0
  139. tan/core/run.py +161 -0
  140. tan/core/scaffold.py +2203 -0
  141. tan/core/scaffold_selftest_identity.py +225 -0
  142. tan/core/sdk_default_registry.py +565 -0
  143. tan/core/sdk_discovery.py +996 -0
  144. tan/core/sdk_removal.py +465 -0
  145. tan/core/serial_actions.py +175 -0
  146. tan/core/serial_capture.py +185 -0
  147. tan/core/serial_url.py +45 -0
  148. tan/core/setools.py +608 -0
  149. tan/core/setools_scratch.py +243 -0
  150. tan/core/shapes.py +194 -0
  151. tan/core/size.py +610 -0
  152. tan/core/som_buildability.py +204 -0
  153. tan/core/som_schema_version.py +62 -0
  154. tan/core/subprocess_env.py +201 -0
  155. tan/core/sysbuild_image.py +94 -0
  156. tan/core/system_manifest.py +827 -0
  157. tan/core/text_layout.py +73 -0
  158. tan/core/timestamp.py +182 -0
  159. tan/core/tool_lookup.py +383 -0
  160. tan/core/toolchain_pin.py +117 -0
  161. tan/core/toolchain_provision.py +1224 -0
  162. tan/core/trusted_path.py +134 -0
  163. tan/core/uboot_breakin.py +154 -0
  164. tan/core/uri_reference.py +415 -0
  165. tan/core/user_defines.py +152 -0
  166. tan/core/venv.py +332 -0
  167. tan/core/west_patches.py +143 -0
  168. tan/core/west_workspace_refusal.py +159 -0
  169. tan/core/zephyr_env.py +124 -0
  170. tan/env.py +185 -0
  171. tan/envelope.py +647 -0
  172. tan/exit_codes.py +12 -0
  173. tan/lazy_typer.py +185 -0
  174. tan/model/__init__.py +2 -0
  175. tan/model/_gen_fixture.py +132 -0
  176. tan/model/adapters/__init__.py +116 -0
  177. tan/model/adapters/cpu.py +26 -0
  178. tan/model/adapters/deepx.py +94 -0
  179. tan/model/adapters/drpai.py +242 -0
  180. tan/model/adapters/ethos_u.py +951 -0
  181. tan/model/adapters/executorch.py +55 -0
  182. tan/model/analyze.py +694 -0
  183. tan/model/build.py +304 -0
  184. tan/model/check.py +682 -0
  185. tan/model/manifest.py +625 -0
  186. tan/model/measure.py +216 -0
  187. tan/model/npu_ops_validate.py +130 -0
  188. tan/model/package.py +125 -0
  189. tan/model/perf.py +635 -0
  190. tan/model/perf_apply.py +721 -0
  191. tan/model/prep.py +193 -0
  192. tan/model/sram_fit.py +502 -0
  193. tan/model/targets.py +374 -0
  194. tan/model/tensorio.py +287 -0
  195. tan/net.py +73 -0
  196. tan/output_format.py +213 -0
  197. tan/planner/__init__.py +219 -0
  198. tan/planner/aperture.py +202 -0
  199. tan/planner/buildplan.py +1092 -0
  200. tan/planner/camera_owner.py +192 -0
  201. tan/planner/cameras.py +132 -0
  202. tan/planner/carveout.py +755 -0
  203. tan/planner/cli.py +274 -0
  204. tan/planner/headers.py +315 -0
  205. tan/planner/kconfig.py +2435 -0
  206. tan/planner/kconfig_symbols.py +473 -0
  207. tan/planner/libraries.py +677 -0
  208. tan/planner/link_target.py +237 -0
  209. tan/planner/linux_ownership.py +216 -0
  210. tan/planner/loader.py +1525 -0
  211. tan/planner/manifest.py +116 -0
  212. tan/planner/memory.py +161 -0
  213. tan/planner/memregion.py +56 -0
  214. tan/planner/models.py +533 -0
  215. tan/planner/orchestrator.py +874 -0
  216. tan/planner/ownership.py +203 -0
  217. tan/planner/partition.py +1160 -0
  218. tan/planner/paths.py +32 -0
  219. tan/planner/plain_slice.py +139 -0
  220. tan/planner/project_emit/__init__.py +60 -0
  221. tan/planner/project_emit/bom_netlist.py +375 -0
  222. tan/planner/project_emit/dts.py +580 -0
  223. tan/planner/project_emit/hw_info.py +221 -0
  224. tan/planner/project_emit/native_sim.py +136 -0
  225. tan/planner/project_emit/west_libs.py +213 -0
  226. tan/planner/project_loader.py +264 -0
  227. tan/planner/sdk_capability.py +203 -0
  228. tan/planner/sdk_compat.py +395 -0
  229. tan/planner/secure.py +350 -0
  230. tan/planner/slugs.py +394 -0
  231. tan/planner/som_metadata.py +325 -0
  232. tan/planner/strict_loaders.py +131 -0
  233. tan/planner/template.py +1037 -0
  234. tan/planner/template_pins.py +535 -0
  235. tan/planner/template_rewrite.py +714 -0
  236. tan/planner/topology.py +208 -0
  237. tan/planner/validate.py +365 -0
  238. tan/planner/whole_device_alias.py +67 -0
  239. tan/planner/zephyr_board.py +3510 -0
  240. tan/planner_cli.py +97 -0
  241. tan/planner_emit.py +617 -0
  242. tan/planner_root.py +119 -0
  243. tan/soc_ref.py +58 -0
  244. tan/templates/__init__.py +34 -0
  245. tan/templates/vendored/MANIFEST.md +1862 -0
  246. tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
  247. tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
  248. tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
  249. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
  250. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
  251. tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
  252. tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
  253. tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
  254. tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
  255. tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
  256. tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
  257. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
  258. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
  259. tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
  260. tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
  261. tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
  262. tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
  263. tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
  264. tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
  265. tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
  266. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
  267. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
  268. tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
  269. tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
  270. tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
  271. tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
  272. tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
  273. tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
  274. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
  275. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
  276. tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
  277. tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
  278. tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
  279. tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
  280. tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
  281. tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
  282. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
  283. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
  284. tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
  285. tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
  286. tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
  287. tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
  288. tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
  289. tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
  290. tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
  291. tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
  292. tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
  293. tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
  294. tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
  295. tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
  296. tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
  297. tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
  298. tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
  299. tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
  300. tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
  301. tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
  302. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
  303. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
  304. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
  305. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
  306. tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
  307. tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
  308. tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
  309. tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
  310. tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
  311. tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
  312. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
  313. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
  314. tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
  315. tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
  316. tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
  317. tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
  318. tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
  319. tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
  320. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
  321. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
  322. tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
  323. tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
  324. tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
  325. tan/version.py +102 -0
  326. tan_cli-0.7.0.dist-info/METADATA +125 -0
  327. tan_cli-0.7.0.dist-info/RECORD +330 -0
  328. tan_cli-0.7.0.dist-info/WHEEL +5 -0
  329. tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
  330. tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/model/measure.py ADDED
@@ -0,0 +1,216 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Host reference run + A/B compare (tan-cli#1287, host tier only).
3
+
4
+ Ported from alp-sdk `integration/model-edge-ai` (`scripts/alp_model/measure.py`,
5
+ alp-sdk#933). The host run (`backend="cpu-host"`) is a FUNCTIONAL + host-latency
6
+ reference, NOT the target SoM's performance. The on-device tier (target
7
+ latency, peak arena SRAM, per-rail energy via the on-board monitor IC) is the
8
+ bench-gated follow-on that fills the SAME `RunResult` schema with real numbers;
9
+ the host run leaves `power_mj`/`peak_sram_kib` `None` and nothing here
10
+ fabricates them; the device tier (`tan.core.model_device`) fills `peak_sram_kib`
11
+ from the app's ENERGY-CFG `sram_peak_bytes` and energy from its capture. The energy-integration math (`integrate_energy`, `windowed_delta`,
12
+ `EnergyMeasurement`) is the on-device tier's, ported unchanged and PURE; the
13
+ estimate-vs-measured feedback loop is not ported yet.
14
+
15
+ `numpy`/`onnxruntime` (the optional `model` extra) are imported inside the
16
+ functions that use them.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from statistics import median
23
+ from time import perf_counter
24
+ from typing import Any, Sequence
25
+
26
+
27
+ class MeasureError(Exception):
28
+ """The model could not be loaded or run for measurement."""
29
+
30
+
31
+ # (t_seconds, power_watts) — power_watts is an on-board monitor Power-register (3h) read.
32
+ PowerSample = tuple[float, float]
33
+
34
+
35
+ def integrate_energy(samples: Sequence[PowerSample]) -> float:
36
+ """Trapezoidal time-integration of power samples into millijoules:
37
+ E = Σ 0.5*(P_i + P_i+1)*Δt over consecutive samples (Δt in seconds,
38
+ P in watts -> joules, *1000 -> mJ). Samples must be in ascending t order
39
+ (the on-board monitor has no on-chip energy accumulator, so this is the whole
40
+ point of taking repeated Power-register samples host-side).
41
+ <2 samples -> 0.0 (no interval to integrate; not an error — a single
42
+ reading carries no energy information, so returning 0.0 lets a caller
43
+ that hasn't collected a real window yet compose without special-casing)."""
44
+ if len(samples) < 2:
45
+ return 0.0
46
+ energy_j = 0.0
47
+ for (t0, p0), (t1, p1) in zip(samples, samples[1:]):
48
+ energy_j += 0.5 * (p0 + p1) * (t1 - t0)
49
+ return energy_j * 1000.0
50
+
51
+
52
+ def windowed_delta(active_samples: Sequence[PowerSample],
53
+ idle_samples: Sequence[PowerSample], n_inferences: int) -> float:
54
+ """Idle-baseline-subtracted energy per inference, in mJ: the delta between
55
+ an active (inferring) window and an idle window, divided by how many
56
+ inferences ran in the active window.
57
+
58
+ Precondition: active_samples and idle_samples MUST cover equal wall-clock
59
+ duration (same integration window length). This is a subtraction of two
60
+ integrals, not a rate comparison -- unequal durations bias the baseline
61
+ (e.g. a longer idle window subtracts more idle energy than the active
62
+ window actually contains). The caller/firmware is responsible for taking
63
+ symmetric windows; this function does not (and should not) assert it,
64
+ since real hardware timing has jitter that would make a hard equality
65
+ check false-trip."""
66
+ if n_inferences < 1:
67
+ raise ValueError(f"n_inferences must be >= 1, got {n_inferences}")
68
+ return (integrate_energy(active_samples) - integrate_energy(idle_samples)) / n_inferences
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class EnergyMeasurement:
73
+ """A real bench-measured energy result — never fabricate one; `run_host()`
74
+ leaves `RunResult.energy` as None instead. `source`/`scope` default to (and
75
+ are validated as) the honest labels: this is a board-level carrier-rail
76
+ delta, NEVER an isolated NPU/U85/U55/M55 silicon energy figure."""
77
+ value_mj_per_inference: float
78
+ rails: list[str]
79
+ n_inferences: int
80
+ window_ms: float
81
+ sample_count: int
82
+ spread_mj: float | None = None # std/spread across M repeated windows;
83
+ # None for a single window
84
+ source: str = "measured"
85
+ scope: str = "carrier-rail-delta"
86
+
87
+ def __post_init__(self) -> None:
88
+ if self.source != "measured" or self.scope != "carrier-rail-delta":
89
+ raise ValueError(
90
+ "EnergyMeasurement is board-level-delta only: source must be "
91
+ "'measured' and scope 'carrier-rail-delta' — never label this "
92
+ "NPU/U85/U55/M55 silicon energy.")
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class RunResult:
97
+ backend: str # "cpu-host" (reference) | on-device backend later
98
+ latency_ms: float # median wall-clock per inference
99
+ output_argmax: int | None
100
+ peak_sram_kib: float | None # None on host — on-device only
101
+ power_mj: float | None # None on host — on-board monitor read (HW-gated)
102
+ runs: int
103
+ energy: EnergyMeasurement | None = None # None until a real bench run populates it
104
+ # Set when the caller's sample was cast to the model input dtype (tan-cli#1497).
105
+ input_cast: str | None = None
106
+
107
+
108
+ _ORT_DTYPES = {
109
+ "float": "float32", "float16": "float16", "double": "float64",
110
+ "int64": "int64", "int32": "int32", "int16": "int16", "int8": "int8",
111
+ "uint64": "uint64", "uint32": "uint32", "uint16": "uint16", "uint8": "uint8",
112
+ "bool": "bool",
113
+ }
114
+
115
+
116
+ def _np_dtype(ort_type: str) -> str:
117
+ """`tensor(int64)` -> `int64`; anything unrecognised (sequences, strings)
118
+ falls back to float32, the previous behaviour (tan-cli#1486)."""
119
+ inner = ort_type[ort_type.find("(") + 1:ort_type.rfind(")")] if "(" in ort_type else ort_type
120
+ return _ORT_DTYPES.get(inner, "float32")
121
+
122
+
123
+ def _sample(shape: list, ort_type: str, seed: int) -> Any:
124
+ import numpy as np
125
+ dtype = _np_dtype(ort_type)
126
+ dims = [(1 if not isinstance(d, int) else d) for d in shape]
127
+ if np.dtype(dtype).kind == "f":
128
+ return np.random.default_rng(seed).standard_normal(dims).astype(dtype)
129
+ return np.zeros(dims, dtype=dtype) # token ids / pixels / flags: a valid, deterministic value
130
+
131
+
132
+ def describe_input_cast(sample_dtype: Any, want: str) -> str | None:
133
+ """A sentence when casting a `sample_dtype` sample to @want changes its
134
+ dtype (tan-cli#1497), naming the silent value loss when float becomes
135
+ integer/bool; None when no cast happens."""
136
+ import numpy as np
137
+ have = np.dtype(sample_dtype)
138
+ if have == np.dtype(want):
139
+ return None
140
+ msg = f"input sample dtype {have} was cast to the model input dtype {want}"
141
+ wd = np.dtype(want)
142
+ if wd.kind == "b":
143
+ msg += "; every nonzero value became True"
144
+ elif have.kind == "f" and wd.kind in "iu":
145
+ msg += "; fractional values were truncated toward zero"
146
+ elif not np.can_cast(have, wd, "safe"):
147
+ msg += "; precision or range may be lost"
148
+ return msg
149
+
150
+
151
+ def default_input(onnx_path: Path, *, seed: int = 0) -> Any:
152
+ """Deterministic random sample matching the model's first input shape.
153
+ Dynamic dims collapse to 1 (intent: a dynamic BATCH dim; a symbolic H/W
154
+ would also become 1). Wrapped in MeasureError so a bad model fails clean."""
155
+ import numpy as np
156
+ import onnxruntime as ort
157
+ try:
158
+ sess = ort.InferenceSession(str(onnx_path), providers=["CPUExecutionProvider"])
159
+ first = sess.get_inputs()[0]
160
+ return _sample(first.shape, first.type, seed)
161
+ except Exception as exc:
162
+ raise MeasureError(f"could not load model {Path(onnx_path).name}: {exc}") from exc
163
+
164
+
165
+ def run_host(onnx_path: Path, input_array: Any, *, runs: int = 20) -> RunResult:
166
+ import numpy as np
167
+ import onnxruntime as ort
168
+ try:
169
+ sess = ort.InferenceSession(str(onnx_path), providers=["CPUExecutionProvider"])
170
+ inputs = sess.get_inputs()
171
+ # The sample feeds the FIRST input in that input's own dtype; any further
172
+ # inputs get a deterministic default sample (tan-cli#1486).
173
+ raw = np.asarray(input_array)
174
+ want = _np_dtype(inputs[0].type)
175
+ input_cast = describe_input_cast(raw.dtype, want)
176
+ feed = {inputs[0].name: raw.astype(want)}
177
+ for extra in inputs[1:]:
178
+ feed[extra.name] = _sample(extra.shape, extra.type, 0)
179
+ out = sess.run(None, feed)[0] # warm-up + a real output
180
+ times = []
181
+ for _ in range(max(1, runs)):
182
+ t0 = perf_counter()
183
+ sess.run(None, feed)
184
+ times.append((perf_counter() - t0) * 1000.0)
185
+ except Exception as exc:
186
+ raise MeasureError(f"host run failed: {exc}") from exc
187
+ return RunResult(backend="cpu-host", latency_ms=round(median(times), 3),
188
+ output_argmax=int(np.asarray(out).ravel().argmax()),
189
+ peak_sram_kib=None, power_mj=None, runs=len(times),
190
+ input_cast=input_cast)
191
+
192
+
193
+ @dataclass(frozen=True)
194
+ class ABComparison:
195
+ faster: str # "a" | "b" | "tie"
196
+ latency_ratio: float | None # b.latency_ms / a.latency_ms; None if a.latency_ms == 0
197
+ # (avoids emitting non-finite JSON `Infinity`)
198
+ a_latency_ms: float
199
+ b_latency_ms: float
200
+ size_delta_bytes: int | None # size_b - size_a
201
+
202
+
203
+ def compare(a: RunResult, b: RunResult, *, size_a: int | None = None,
204
+ size_b: int | None = None) -> ABComparison:
205
+ if a.latency_ms < b.latency_ms:
206
+ faster = "a"
207
+ elif b.latency_ms < a.latency_ms:
208
+ faster = "b"
209
+ else:
210
+ faster = "tie"
211
+ ratio = (b.latency_ms / a.latency_ms) if a.latency_ms else None
212
+ delta = (size_b - size_a) if (size_a is not None and size_b is not None) else None
213
+ return ABComparison(faster=faster,
214
+ latency_ratio=round(ratio, 4) if ratio is not None else None,
215
+ a_latency_ms=a.latency_ms, b_latency_ms=b.latency_ms,
216
+ size_delta_bytes=delta)
@@ -0,0 +1,130 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Schema validation for the per-NPU op-support tables `tan model check`
3
+ reads (tan-cli#1298).
4
+
5
+ `metadata/npu_ops/**` used to have no schema, so `analyze.py` carried only
6
+ `isinstance` guards. `metadata/schemas/npu-ops-v1.schema.json` now exists on
7
+ alp-sdk `dev` (alp-sdk#1801 closed, landed with alp-sdk#1470); a bound SDK may
8
+ or may not ship it. This module is the read-path half of tan-cli#964's rule
9
+ for that table family: validate on read, always report, and skip-but-disclose
10
+ when the schema cannot be used -- the policy lives here, the validator stays
11
+ the one in `tan.core.metadata_schema`.
12
+
13
+ Three outcomes for a table that is about to be used:
14
+
15
+ * schema usable, table valid -> used.
16
+ * schema usable, table violates it -> NOT used (skipped like a malformed
17
+ file, so the backend reads `undetermined` unless another table covers the
18
+ variant), with one note naming the file, JSON pointer and what was found.
19
+ * schema absent, unreadable, not valid JSON, or not a valid JSON Schema ->
20
+ the table IS used under `analyze.py`'s isinstance guards, with ONE note per
21
+ backend saying the tables were not validated. A broken schema is no
22
+ evidence against a table, so it is never blamed on one.
23
+
24
+ Only a table that matches the requested variant is validated; a table for
25
+ another variant is neither validated nor mentioned. Selection reads
26
+ `applies_to.variant` off a not-yet-validated document, which is safe because
27
+ `_table_variant` is isinstance-guarded to return `""` for any wrong shape --
28
+ and a table whose variant matches is then validated before it is accepted, so
29
+ an unvalidated document can only ever fail to match, never be scored.
30
+
31
+ The notes ride the static-screen report only. The `--exact` compiled report
32
+ and the bench-point report build their own `notes` and do not carry them.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from pathlib import Path
38
+
39
+ from tan.core.metadata_schema import (
40
+ missing_schema_note,
41
+ npu_ops_schema_path,
42
+ schema_unusable_reason,
43
+ validate_document,
44
+ )
45
+
46
+ # A badly broken table can violate the schema dozens of times; the first few
47
+ # name the problem, the rest just bloat the envelope.
48
+ _MAX_VIOLATIONS_SHOWN = 5
49
+
50
+
51
+ def _posix(value: object) -> str:
52
+ return str(value).replace("\\", "/")
53
+
54
+
55
+ class TableValidator:
56
+ """Validates the tables of ONE backend directory against the npu-ops
57
+ schema, working out whether the schema is usable lazily -- on the first
58
+ table that is actually about to be used -- and disclosing "not validated"
59
+ at most once.
60
+
61
+ Every path in a note is written relative to the metadata root, with
62
+ forward slashes (`npu_ops/ethos_u/u85@vela-5.2.0.json`,
63
+ `schemas/npu-ops-v1.schema.json`): the note rides the envelope, and an
64
+ absolute host path in it is both noise and a leak. (It also made a test
65
+ whose tmp dir name contained a word the note-scanning tests forbid fail
66
+ on CI, tan-cli#1298.)"""
67
+
68
+ def __init__(self, metadata_root: Path | str, table_dir: Path) -> None:
69
+ self._root = Path(metadata_root)
70
+ self._schema_path = npu_ops_schema_path(metadata_root)
71
+ self._table_dir = table_dir
72
+ self._probed = False
73
+ self._usable = False
74
+
75
+ def _rel(self, path: Path | str) -> str:
76
+ try:
77
+ return Path(path).relative_to(self._root).as_posix()
78
+ except ValueError:
79
+ return Path(path).name
80
+
81
+ def _scrub(self, text: str) -> str:
82
+ """@text with the metadata root's absolute spellings replaced -- a
83
+ defence for a reason string that carries a path despite
84
+ `schema_unusable_reason` already dropping the OS filename."""
85
+ for spelling in {str(self._root), self._root.as_posix(), _posix(self._root)}:
86
+ text = text.replace(spelling, "<metadata>")
87
+ return text
88
+
89
+ def _probe(self, notes: list[str]) -> None:
90
+ self._probed = True
91
+ where = f"{self._rel(self._table_dir)}: not validated -- "
92
+ schema = self._rel(self._schema_path)
93
+ reason = schema_unusable_reason(self._schema_path)
94
+ if reason is not None:
95
+ notes.append(f"{where}the schema at {schema} could not be loaded "
96
+ f"({self._scrub(reason)}); the tables are used under "
97
+ "the built-in shape checks only")
98
+ return
99
+ try:
100
+ absent = missing_schema_note(self._schema_path, source=self._table_dir) is not None
101
+ except OSError as exc:
102
+ # `Path.is_file()` raises on a permission-denied ancestor on
103
+ # Python 3.12/3.13 (see the comment above `_is_table_file` in
104
+ # analyze.py): "cannot tell" is not "absent".
105
+ notes.append(f"{where}the schema at {schema} could not be inspected "
106
+ f"({exc.strerror or 'OS error'}); the tables are used "
107
+ "under the built-in shape checks only")
108
+ return
109
+ if absent:
110
+ notes.append(f"{where}no schema at {schema} in this checkout")
111
+ return
112
+ self._usable = True
113
+
114
+ def accepts(self, doc: object, table_path: Path, notes: list[str]) -> bool:
115
+ """True when *table_path* (already parsed to *doc*) may be used.
116
+ Appends to *notes* the not-validated disclosure (once) or this
117
+ table's violation note."""
118
+ if not self._probed:
119
+ self._probe(notes)
120
+ if not self._usable:
121
+ return True
122
+ errors = validate_document(doc, self._schema_path, self._rel(table_path))
123
+ if not errors:
124
+ return True
125
+ shown = "; ".join(errors[:_MAX_VIOLATIONS_SHOWN])
126
+ extra = len(errors) - _MAX_VIOLATIONS_SHOWN
127
+ more = f" (+{extra} more)" if extra > 0 else ""
128
+ notes.append(f"npu-ops table skipped, it fails npu-ops-v1.schema.json: "
129
+ f"{shown}{more}. This table is not used.")
130
+ return False
tan/model/package.py ADDED
@@ -0,0 +1,125 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Assemble/parse the .alpmodel binary container (header + CBOR manifest + blobs).
3
+
4
+ `CONTAINER_VERSION` describes the BINARY FRAME below -- the 24-byte header's
5
+ field layout and the 8-byte blob-table entries -- and nothing about which keys
6
+ the CBOR manifest carries. Adding a manifest key is therefore NOT a container
7
+ change: alp-sdk's on-device reader skips keys it does not know
8
+ (`src/common/alp_model.c`, see `tan.model.manifest`'s module docstring for the
9
+ measured evidence), while a CONTAINER_VERSION it does not know is a hard
10
+ ALP_ERR_VERSION refusal for every fielded device. Do not move this constant to
11
+ announce a manifest field.
12
+ """
13
+ from __future__ import annotations
14
+ import os
15
+ import struct
16
+ from pathlib import Path
17
+ from .manifest import Manifest
18
+
19
+ MAGIC = b"ALPM"
20
+ CONTAINER_VERSION = 1
21
+ _HEADER = struct.Struct("<4sHHIIII") # magic, ver, flags, mft_off, mft_len, tbl_off, blob_count
22
+ _HEADER_SIZE = _HEADER.size # 24
23
+ _TBL_ENTRY = struct.Struct("<II") # blob off, blob len
24
+
25
+
26
+ def write_package(mft: Manifest, blobs: list[bytes]) -> bytes:
27
+ mft_bytes = mft.to_cbor()
28
+ mft_off = _HEADER_SIZE
29
+ tbl_off = mft_off + len(mft_bytes)
30
+ blobs_off = tbl_off + _TBL_ENTRY.size * len(blobs)
31
+
32
+ table, blob_region, cursor = b"", b"", blobs_off
33
+ for b in blobs:
34
+ table += _TBL_ENTRY.pack(cursor, len(b))
35
+ blob_region += b
36
+ cursor += len(b)
37
+
38
+ header = _HEADER.pack(MAGIC, CONTAINER_VERSION, 0,
39
+ mft_off, len(mft_bytes), tbl_off, len(blobs))
40
+ return header + mft_bytes + table + blob_region
41
+
42
+
43
+ def _unpack_header(head: bytes) -> tuple[int, int, int, int]:
44
+ """(mft_off, mft_len, tbl_off, blob_count) from a validated 24-byte header."""
45
+ magic, ver, _flags, mft_off, mft_len, tbl_off, n = _HEADER.unpack_from(head, 0)
46
+ if magic != MAGIC:
47
+ raise ValueError(f"bad magic {magic!r}")
48
+ if ver != CONTAINER_VERSION:
49
+ raise ValueError(f"unsupported container version {ver}")
50
+ return mft_off, mft_len, tbl_off, n
51
+
52
+
53
+ def read_manifest_file(path: Path) -> Manifest:
54
+ """The manifest of an on-disk package, WITHOUT reading its blobs.
55
+
56
+ `read_package` needs the whole container in memory; a caller that only
57
+ wants to read back what a package says about itself (`tan model build`
58
+ reporting a shipped target's caveats) would then pay a full copy of every
59
+ compiled blob -- megabytes for a real vision model -- to read a few
60
+ hundred bytes of CBOR. This seeks to the manifest region instead, so the
61
+ cost is O(manifest), not O(package).
62
+
63
+ Reading it back off disk, rather than reporting the in-memory object that
64
+ was just written, is deliberate: it reports what the ARTIFACT says. A
65
+ caveat lost by the encoder is then visibly missing from the build output
66
+ instead of being narrated from a manifest that never reached the file."""
67
+ with open(path, "rb") as f:
68
+ head = f.read(_HEADER_SIZE)
69
+ if len(head) < _HEADER_SIZE:
70
+ raise ValueError(f"truncated container: {len(head)} bytes, "
71
+ f"header is {_HEADER_SIZE}")
72
+ mft_off, mft_len, _tbl_off, _n = _unpack_header(head)
73
+ # Bound the read against the real file size BEFORE issuing it.
74
+ # `mft_off`/`mft_len` are untrusted u32s off the wire, and unlike
75
+ # `read_package`'s slice of an already-loaded buffer, `f.read(n)`
76
+ # allocates n bytes -- an `mft_len` of 0xFFFFFFFF would ask for 4 GiB
77
+ # from a 5 KiB file. Compared subtraction-style, the same shape
78
+ # alp-sdk's reader uses for the same fields (`src/common/alp_model.c`:
79
+ # "adding the untrusted u32 offset and length first can wrap").
80
+ size = os.fstat(f.fileno()).st_size
81
+ if mft_off > size or mft_len > size - mft_off:
82
+ raise ValueError(f"manifest region {mft_off}+{mft_len} runs past "
83
+ f"the end of a {size}-byte container")
84
+ f.seek(mft_off)
85
+ blob = f.read(mft_len)
86
+ if len(blob) != mft_len:
87
+ raise ValueError(f"truncated manifest: {len(blob)} of {mft_len} bytes")
88
+ return Manifest.from_cbor(blob)
89
+
90
+
91
+ def read_package(raw: bytes) -> tuple[Manifest, list[bytes]]:
92
+ """The manifest AND blobs of an in-memory `.alpmodel` buffer.
93
+
94
+ Every region this reads (`mft_off`/`mft_len`, the blob table itself, and
95
+ each table entry's `off`/`length`) is bounds-checked against `len(raw)`
96
+ before the matching slice, using the same subtraction-style compare
97
+ `read_manifest_file` uses (`off > size or length > size - off`, chosen
98
+ there because adding the untrusted u32 offset and length first can wrap).
99
+ The motivation for checking at all differs, though: `read_manifest_file`
100
+ checks to avoid an over-allocation risk (`f.read(n)` allocates n bytes up
101
+ front), while slicing `bytes` cannot over-allocate but also does not
102
+ raise -- `b"AB"[0:999999]` is `b"AB"`, silently truncated, not a crash
103
+ (tan-cli#1045). A corrupted or adversarial header/table entry must be
104
+ refused here, the same way `read_manifest_file`'s file-based sibling
105
+ already refuses one."""
106
+ size = len(raw)
107
+ if size < _HEADER_SIZE:
108
+ raise ValueError(f"truncated container: {size} bytes, header is {_HEADER_SIZE}")
109
+ mft_off, mft_len, tbl_off, n = _unpack_header(raw)
110
+ if mft_off > size or mft_len > size - mft_off:
111
+ raise ValueError(f"manifest region {mft_off}+{mft_len} runs past "
112
+ f"the end of a {size}-byte container")
113
+ mft = Manifest.from_cbor(raw[mft_off:mft_off + mft_len])
114
+ tbl_len = n * _TBL_ENTRY.size
115
+ if tbl_off > size or tbl_len > size - tbl_off:
116
+ raise ValueError(f"blob table region {tbl_off}+{tbl_len} (for {n} "
117
+ f"entries) runs past the end of a {size}-byte container")
118
+ blobs = []
119
+ for i in range(n):
120
+ off, length = _TBL_ENTRY.unpack_from(raw, tbl_off + i * _TBL_ENTRY.size)
121
+ if off > size or length > size - off:
122
+ raise ValueError(f"blob {i} region {off}+{length} runs past "
123
+ f"the end of a {size}-byte container")
124
+ blobs.append(raw[off:off + length])
125
+ return mft, blobs