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
@@ -0,0 +1,951 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Arm Ethos-U (Vela) compiler adapter.
3
+
4
+ Wraps the `vela` CLI from `ethos-u-vela` (the `model-compile` optional
5
+ dependency). is_available() is True when `vela` is on PATH; compile() shells out
6
+ for the given accelerator-config and reads back `<stem>_vela.tflite`. The
7
+ arena/peak-SRAM footprint is parsed from vela's summary CSV for THAT run, and a
8
+ successful compile that placed operators on the NPU but reports no SRAM working
9
+ set is a hard error, never a silent zero (see `_refuse_zero_sram_footprint`).
10
+
11
+ compile() NEVER raises on a clean vela exit *for placement reasons*, including a
12
+ full CPU fallback -- vela's own exit code is 0 whether it placed every operator
13
+ on the NPU or none of them (measured: `vela float32_fc.tflite
14
+ --accelerator-config ethos-u85-256` on a float32 FULLY_CONNECTED model prints
15
+ "NPU operators = 0 (0.0%)" and still exits 0). `_parse_vela_placement` reads
16
+ vela's own per-run placement summary (always printed to stdout,
17
+ `ethosu.vela.stats_writer.print_performance_metrics_common`'s "CPU/NPU
18
+ operators = N (P%)" lines) so a caller never has to infer placement from the
19
+ mere absence of an exception -- see `tan.model.check`'s
20
+ `_report_from_vela_compile`, the actual consumer of `Blob.npu_op_count`/
21
+ `cpu_op_count`.
22
+
23
+ THE MEMORY PROFILE (alp-sdk #1470): this adapter passes `--memory-mode` when
24
+ its caller resolved one, and today every caller does -- it is a SILICON fact,
25
+ published per part in the SoC spec's `npu_toolchain.vela.memory_mode`
26
+ (`Sram_Only` on the Alif Ensemble parts) and
27
+ carried here as `TargetSpec.vela_memory_mode`. It is never guessed: a caller
28
+ that resolved none gets the flagless invocation, byte for byte.
29
+
30
+ That flag is the one that matters, and the reason is measured. On the committed
31
+ `tests/fixtures/models/tiny_int8.tflite` at `ethos-u85-256`, `ethos-u-vela`
32
+ 5.1.0 reports:
33
+
34
+ * with no profile flags -- `sram_memory_used = 0.0`,
35
+ `dram_memory_used = 0.265625` (under `Ethos_U85_SYS_DRAM_Mid` /
36
+ `Dedicated_Sram_384KB`, both of which vela warns it defaulted)
37
+ * with `--memory-mode Sram_Only` -- `sram_memory_used = 0.03125`,
38
+ `dram_memory_used = 0.0`, `on_chip_flash_memory_used = 0.234375`
39
+
40
+ Both exit 0. The first is the shape tan-cli#789 had to REFUSE: vela's built-in
41
+ default profile is DRAM-backed (`Ethos_U85_SYS_DRAM_Mid` /
42
+ `Dedicated_Sram_384KB`, `weights_storage_area=DRAM` in the summary CSV) and an
43
+ Alif Ensemble E-series module is MRAM + SRAM with no DRAM at all, so the whole
44
+ working set landed in memory the part does not have and the SRAM figure alp-sdk
45
+ sizes an arena from came back zero. Adding `--system-config
46
+ Ethos_U85_SYS_Flash_High` on top of `--memory-mode Sram_Only` changes NO memory
47
+ figure (measured on `tiny_int8.tflite` AND on the 44-op
48
+ `person_detect_int8.tflite`, both at `ethos-u85-256`, against the default
49
+ `Ethos_U85_SYS_DRAM_Mid`).
50
+
51
+ THAT INVARIANCE IS SCOPED TO `Sram_Only` AND IS NOT A GENERAL RULE. The two
52
+ flags compose in two levels, verbatim from vela 5.1.0's own `vela.ini`: a
53
+ `Memory_Mode` section assigns const/arena/cache to AXI PORTS
54
+ (`[Memory_Mode.Sram_Only]` -> `const_mem_area` = `arena_mem_area` =
55
+ `cache_mem_area` = `Axi0`, `vela.ini:235-238`), and a `System_Config` section
56
+ maps those ports to MEMORY AREAS (`axi0_port=`/`axi1_port=`, `vela.ini:60-117`).
57
+ Under `Sram_Only` all three sit on `Axi0` and all 11 `System_Config` sections
58
+ vela 5.1.0 ships set `axi0_port=Sram`, so no system config can move anything --
59
+ which is the only reason those two measurements matched. `Shared_Sram` sets
60
+ `const_mem_area=Axi1` (`vela.ini:242-245`); under it the system config decides
61
+ the const region's memory area outright. Measured, `person_detect_int8.tflite` at `ethos-u65-256 --memory-mode
62
+ Shared_Sram`, changing ONLY `--system-config` (KiB, from vela's own summary):
63
+
64
+ * `Ethos_U65_Embedded` (`axi1_port=OffChipFlash`) -- `sram 72.734375`,
65
+ `dram 0.0`, `off_chip_flash 228.265625`
66
+ * `Ethos_U65_Mid_End` (`axi1_port=Dram`) -- `sram 72.734375`,
67
+ `dram 228.3125`, `off_chip_flash 0.0`
68
+ * `Ethos_U65_Client_Server` (`axi1_port=Dram`) -- `sram 72.734375`,
69
+ `dram 228.25`, `off_chip_flash 0.0`
70
+
71
+ 228 KiB of weights moves on the system config alone. So the honest rule is:
72
+ under `Sram_Only` the memory mode decides placement outright and the system
73
+ config is bandwidth/latency only; under an `Axi1` const mode the system config
74
+ decides placement too.
75
+
76
+ `--system-config` is therefore still not passed by default, and that is
77
+ deliberate rather than pending. The names alp-sdk's own examples use
78
+ (`Ethos_U85_SRAM_Only`, `RTSS_HE_SRAM_Only`;
79
+ `examples/aen/aen-npu-inference-alp/CMakeLists.txt:42-43` and its `-u55`
80
+ sibling) live ONLY in Alif's PROPRIETARY `ensemble_vela.ini`, which alp-sdk does
81
+ not redistribute, and handing vela a section it cannot resolve is a hard rc=1,
82
+ verbatim: `Section System_Config.Ethos_U85_SRAM_Only not found in Vela config
83
+ file`. So the SoC spec flags those as vendor-gated and `resolve_targets`
84
+ withholds them from the field this adapter passes freely
85
+ (`tan.model.targets._vela_profile`); vela's own default system config is used
86
+ and SAID SO through `_default_profile_caveats`, which now distinguishes the two
87
+ flags rather than blaming the whole profile on vela. Never invent a profile
88
+ here; a wrong one compiles a command stream for hardware the module does not
89
+ have.
90
+
91
+ A LICENSED CUSTOMER CAN SUPPLY THAT FILE, through `ALP_VELA_CONFIG` and never
92
+ through `board.yaml` (the path is environment, not hardware). It buys the
93
+ vendor-tuned profile only as a complete set: `--config` + the part's vendor
94
+ `System_Config` + its memory mode, all three or none -- see `compile()`, where
95
+ each half of that rule is pinned to a measured `ethos-u-vela` 5.1.0 rc. No SoC
96
+ spec names a vendor `System_Config` today, so the mechanism is wired and does
97
+ nothing, which is the only correct thing it can do: passing `--config` alone is
98
+ rc=1 and naming a profile nobody published would be an invention.
99
+
100
+ WHO THE OLD DEFAULT HIT (tan-cli#789 review), i.e. what the memory mode fixes:
101
+ the DRAM-backed built-in default hit every U85 part. Measured, real
102
+ `ethos-u-vela` 5.1.0 over the committed fixture, this refused with no memory
103
+ mode:
104
+
105
+ * `ethos-u85-256` -> `Ethos_U85_SYS_DRAM_Mid` / `Dedicated_Sram_384KB`
106
+ (E1M-AEN401 / E1M-AEN601 / E1M-AEN801, Alif Ensemble E4/E6/E8)
107
+
108
+ while every `ethos-u55-*` config (E1M-AEN301/501/701 and the U55 targets of the
109
+ three AEN SKUs above) resolved to the SRAM-backed
110
+ `Ethos_U55_High_End_Embedded` and reported a real footprint. The refusal is
111
+ kept, not deleted: a part whose spec carries no profile, or a compile that
112
+ reports no SRAM even under its module's own memory mode, must still be refused
113
+ rather than shipped as a zero. `_refuse_zero_sram_footprint` names the profile
114
+ the run ITSELF reported (`_parse_vela_profile`), never a hardcoded Alif one --
115
+ a user of another vendor's part must not read an error blaming
116
+ `Ethos_U85_SYS_DRAM_Mid`.
117
+
118
+ ...AND NEITHER IS THE REMEDY (tan-cli#789 review (g)). Naming the profile per
119
+ run only half-closed that: the REMEDY sentence still told every reader the
120
+ profile "lives in the proprietary ensemble_vela.ini", which is an Alif fact.
121
+ A part whose vela invocation carries no proprietary `.ini` would have been sent
122
+ after another vendor's file, which is not what its silicon needs and not where
123
+ its profile comes from. The memory mode tan passes comes straight from the SoC
124
+ spec's own `npu_toolchain.vela` block, so there is nothing to advise a reader
125
+ to do by hand.
126
+
127
+ BOTH HALVES OF THE REFUSAL'S EVIDENCE NOW COME FROM METADATA, not from prose in
128
+ this file: WHICH vendor file (if any) to name is the SoC spec's own
129
+ `npu_toolchain.vela.vendor_config_filename` (`@vela_vendor_config_filename`),
130
+ and WHY a DRAM placement is wrong here is `@soc_declares_dram`, resolved from
131
+ its `external_memory_interfaces[]` -- `alif:ensemble:e8` lists exactly `HexSPI`
132
+ and `SD/eMMC`, `renesas:rzv2n:n44` lists `LPDDR4/4X`. Each is then right for every
133
+ part automatically, including one nobody has looked at yet.
134
+
135
+ Neither is read from `metadata/` HERE: `resolve_targets` already has the SoC
136
+ spec open, an adapter that reads the SDK's fact tree would be a second
137
+ unversioned reader of it, and a fact resolved once and threaded cannot disagree
138
+ with itself between the target and the artifact it produced."""
139
+ from __future__ import annotations
140
+ import csv
141
+ import math
142
+ import os
143
+ import re
144
+ import shutil
145
+ import subprocess
146
+ from importlib.metadata import PackageNotFoundError, version
147
+ from pathlib import Path
148
+ from typing import NoReturn
149
+
150
+ from tan.core.subprocess_env import spawn_env
151
+ from . import CompilerAdapter, Blob
152
+
153
+ _VELA_TIMEOUT_S = 600 # vela compiles are minutes at most; never unbounded in CI
154
+
155
+ # ethosu.vela.stats_writer.print_performance_metrics_common (always printed to
156
+ # stdout, f=sys.stdout is its own default) emits exactly one "CPU operators ="
157
+ # and one "NPU operators =" line per run, e.g. "NPU operators = 0 (0.0%)". The
158
+ # regex intentionally ignores the printed percentage -- _parse_vela_placement
159
+ # recomputes it from the two integer counts so it never inherits vela's own
160
+ # text-formatting rounding.
161
+ _PLACEMENT_RE = re.compile(r"^(CPU|NPU) operators = (\d+)", re.MULTILINE)
162
+
163
+ # The same network-summary block names the profile vela ACTUALLY resolved --
164
+ # "System configuration Ethos_U85_SYS_DRAM_Mid" / "Memory mode
165
+ # Dedicated_Sram_384KB" -- whether it came from a supplied .ini or from vela's
166
+ # built-in default. Read rather than assumed: it is both the summary CSV's own
167
+ # filename suffix and the only honest way to say WHICH profile a figure
168
+ # describes.
169
+ _PROFILE_RE = re.compile(r"^(System configuration|Memory mode) +(\S+) *$", re.MULTILINE)
170
+
171
+ # vela's verbatim warning when it falls back to its own built-in profile.
172
+ # Deliberately NOT matched on the "No configuration file specified" sibling:
173
+ # that one interpolates an absolute path into the host's site-packages, which
174
+ # must never be echoed into a customer-facing envelope.
175
+ _DEFAULTED_RE = re.compile(
176
+ r"^Warning: No (system configuration|memory mode) specified\.", re.MULTILINE)
177
+
178
+ # vela's own names for the two profile flags, as they appear in that warning.
179
+ # Read PER FLAG, not as one boolean: since alp-sdk #1470 tan supplies the
180
+ # memory mode from SoC metadata and still supplies no system config, so a run
181
+ # is routinely half-defaulted -- measured, `--memory-mode Sram_Only` at
182
+ # `ethos-u85-256` still prints "Warning: No system configuration specified.
183
+ # Using a default of Ethos_U85_SYS_DRAM_Mid." and nothing else. Calling that
184
+ # whole run "vela's built-in default profile" would credit vela with the
185
+ # memory mode tan just passed, and would tell a customer the SRAM figure
186
+ # describes a memory model that is not theirs when it now describes exactly
187
+ # theirs.
188
+ _SYSTEM_CONFIG_FLAG = "system configuration"
189
+ _MEMORY_MODE_FLAG = "memory mode"
190
+
191
+ # The memory modes whose CONST/weights area sits on the same AXI port as the
192
+ # arena (`Axi0`), read off vela 5.1.0's own `vela.ini`: `[Memory_Mode.Sram_Only]`
193
+ # is `const_mem_area=arena_mem_area=cache_mem_area=Axi0` (`vela.ini:235-238`),
194
+ # and it is the only shipped section in that shape -- `Shared_Sram` and all four
195
+ # `Dedicated_Sram*` sections set `const_mem_area=Axi1` (`vela.ini:242-269`).
196
+ #
197
+ # It matters for the CAVEAT, not the command line: a `System_Config` maps ports
198
+ # to memory AREAS, so on an `Axi0` const mode a defaulted system config cannot
199
+ # move the const region anywhere (all 11 sections set `axi0_port=Sram`) and the
200
+ # caveat is honestly bandwidth-only, while on an `Axi1` const mode that same
201
+ # default DECIDED where the const region lives -- measured on
202
+ # `person_detect_int8.tflite` at `ethos-u65-256 --memory-mode Shared_Sram`,
203
+ # 228 KiB of weights moves between `off_chip_flash` and `dram` on the system
204
+ # config alone. An UNRECOGNISED name is treated as NOT-Axi0 on purpose: the
205
+ # under-claiming caveat is the safe direction.
206
+ _AXI0_CONST_MEMORY_MODES = frozenset({"Sram_Only"})
207
+
208
+ # `write_summary_metrics_csv_common` writes one `<mem_area>_memory_used` column
209
+ # per memory area the accelerator config declares (stats_writer.py's
210
+ # `area.identifier_name() + "_memory_used"`), so the area names are data, not a
211
+ # fixed list -- matched by shape so a vela version that adds an area is read,
212
+ # not silently dropped.
213
+ _MEM_USED_RE = re.compile(r"^(.+)_memory_used$")
214
+
215
+ # A directory component built from an accel_config that comes out of SoM
216
+ # metadata; anything outside this set is folded to "_" so a malformed
217
+ # accel_config can never walk out of @out_dir.
218
+ _UNSAFE_DIR_CHARS = re.compile(r"[^A-Za-z0-9._-]")
219
+
220
+ # vela's own memory-area name for external DRAM, as it appears in its summary
221
+ # CSV columns (`dram_memory_used`) and therefore as `_parse_vela_summary` keys
222
+ # it -- the area a working set must NOT land in on a part whose SoC spec
223
+ # declares no DRAM interface.
224
+ _DRAM_AREA = "dram"
225
+
226
+ # What the refusal appends to that area's figure when the caller resolved
227
+ # `soc_declares_dram=False` -- i.e. the SoC spec's `external_memory_interfaces`
228
+ # names no DDR/DRAM kind at all (`tan.model.targets._soc_declares_dram`; on
229
+ # `alif:ensemble:e8` it lists exactly `HexSPI` and `SD/eMMC`). Machine-checked
230
+ # per part rather than asserted in prose, so it is silent for a part that
231
+ # declares DRAM (`renesas:rzv2n:n44` -- LPDDR4/4X) or declares nothing.
232
+ #
233
+ # Kept SHORT deliberately: it lands inside a one-line note bounded by
234
+ # `tan.model.check._VELA_REFUSAL_NOTE_BUDGET` (700), against which the maximal
235
+ # refusal measures 691 with this marker and a 17-character
236
+ # `vendor_config_filename` in it (`tests/model/test_check.py::
237
+ # test_the_refusal_note_budget_covers_a_maximal_refusal`, which renders the
238
+ # real template rather than a copy). That filename is the one variable-length
239
+ # input in the whole template, so those 9 characters are the headroom a longer
240
+ # future one has to fit inside.
241
+ _NO_DRAM_MARKER = " (no DRAM interface on this SoC)"
242
+
243
+
244
+ #: Where a licensed customer names their vendor vela config `.ini` -- an
245
+ #: ENVIRONMENT fact, never a hardware one, so it is read here and never from
246
+ #: `board.yaml`. alp-sdk's own schema says why: `models[].compile` is for a
247
+ #: per-model config "the SDK cannot derive", and a vela profile IS derivable
248
+ #: from the SKU; a local absolute path in a committed board file would also
249
+ #: travel to every other machine that opens it. Same shape as the other two
250
+ #: toolchain env vars tan already reads in adapters (`ALP_DRPAI_TVM_HOME`,
251
+ #: `ALP_DEEPX_SDK_HOME`).
252
+ _VELA_CONFIG_ENV = "ALP_VELA_CONFIG"
253
+
254
+
255
+ def _vendor_config_path() -> Path | None:
256
+ """The vendor vela `.ini` this host has, or `None`.
257
+
258
+ `None` for unset, empty, and for a value that does not name a readable
259
+ FILE -- vela would fail on a path it cannot open, and an env var pointing
260
+ at a stale location must degrade to "no vendor config" (Arm's built-in
261
+ profile, which is what makes the arena figures correct in the first place)
262
+ rather than break a build that worked yesterday."""
263
+ raw = os.environ.get(_VELA_CONFIG_ENV)
264
+ if not raw:
265
+ return None
266
+ path = Path(raw).expanduser()
267
+ return path if path.is_file() else None
268
+
269
+
270
+ class VelaFootprintRefused(RuntimeError):
271
+ """vela ran CLEANLY and tan refused the footprint it reported.
272
+
273
+ A distinct type because the two callers must tell it apart from a real
274
+ vela failure, and neither can do that by inspecting a message:
275
+
276
+ * `tan.model.build.build_model` skips THIS target and keeps building the
277
+ SKU's others (tan-cli#789 review BLOCKER 1: one refused
278
+ `ethos-u85-256` used to abort the whole `.alpmodel`, taking the
279
+ `ethos-u55-256` / `ethos-u55-128` / `cpu` targets that compiled fine
280
+ down with it). A genuine `adapter.compile()` failure still fails the
281
+ build loudly -- that is deliberate and unchanged.
282
+ * `tan.model.check`'s `--exact` opens its note "vela compiled cleanly
283
+ for <accel-config> ...", NOT "--exact compile with vela failed (...)"
284
+ (tan-cli#789 review NIT 8) -- vela exited 0; it was the footprint that
285
+ was refused. It also passes the message through at a wider note budget
286
+ because -- unlike a vela subprocess re-raising its own raw stderr --
287
+ this text is authored here, one line, and bounded.
288
+
289
+ The message is a single line by contract (both callers put it in a
290
+ one-line surface): a report note and an `.alpmodel` coverage reason.
291
+
292
+ WHERE THE FIRST BULLET IS ACTUALLY ENFORCED, so nobody re-derives it the
293
+ hard way: the four tests that pin "one refusal costs ONE target" live in
294
+ `tests/model/test_build.py`, which carries a MODULE-LEVEL
295
+ `pytest.mark.skipif` on `ALP_SDK_ROOT` (`test_build.py:44`). So in the bare
296
+ install CI's `gates` job uses -- `pip install -e ./python`, no
297
+ `ALP_SDK_ROOT` -- replacing `build_model`'s `except VelaFootprintRefused`
298
+ body with a bare `raise` passes the whole suite green. The guard is real,
299
+ but it lives in `parity.yml`, which binds `ALP_SDK_ROOT` from
300
+ `PINNED_SDK_TAG` on every `pull_request`/`push`; `ci.yml`'s `gates` job
301
+ binds it only on the release path (its `sdk_parity` input). Do not read a
302
+ green bare run as having exercised this."""
303
+
304
+
305
+ def _vela_version() -> str:
306
+ try:
307
+ return f"vela {version('ethos-u-vela')}"
308
+ except PackageNotFoundError:
309
+ return "vela"
310
+
311
+
312
+ def _run_dir(out_dir: Path, accel_config: str) -> Path:
313
+ """A per-accel-config subdirectory of @out_dir for one vela run.
314
+
315
+ vela names its summary `<stem>_summary_<system_config>.csv`, and the
316
+ system-config is a property of the SILICON FAMILY, not of the accelerator
317
+ config: `ethos-u55-256` and `ethos-u55-128` both resolve to
318
+ `Ethos_U55_High_End_Embedded` and therefore write the SAME filename
319
+ (measured, vela 5.1.0). `build_model` reuses one `out_dir` across every
320
+ config a SoM declares, so a shared directory means one compile's summary
321
+ silently answers for another's -- a u85 target inheriting a u55 compile's
322
+ arena. One directory per run makes that impossible, and keeps vela's
323
+ intermediate output from landing beside the `.alpmodel` package."""
324
+ return out_dir / f"vela-{_UNSAFE_DIR_CHARS.sub('_', accel_config)}"
325
+
326
+
327
+ def _parse_vela_profile(stdout: str) -> tuple[str | None, str | None]:
328
+ """(system_config, memory_mode) vela resolved for this run, from its own
329
+ network-summary block. Either is None when the line is absent (an
330
+ unexpected vela output shape) -- never guessed at."""
331
+ found = dict(_PROFILE_RE.findall(stdout))
332
+ return found.get("System configuration"), found.get("Memory mode")
333
+
334
+
335
+ def _summary_csv(out_dir: Path, stem: str, system_config: str | None) -> Path | None:
336
+ """vela's summary CSV for THIS run: the exact `<stem>_summary_<system_
337
+ config>.csv` name when the run's own system-config was readable, else the
338
+ sole glob match. `None` for no match AND for an ambiguous one -- picking
339
+ `sorted(matches)[0]` out of several is only ever right by accident of sort
340
+ order, and a footprint attributed to the wrong compile is worse than no
341
+ footprint at all (which the caller refuses loudly)."""
342
+ if system_config:
343
+ exact = out_dir / f"{stem}_summary_{system_config}.csv"
344
+ if exact.is_file():
345
+ return exact
346
+ matches = sorted(out_dir.glob(f"{stem}_summary_*.csv"))
347
+ return matches[0] if len(matches) == 1 else None
348
+
349
+
350
+ def _parse_vela_summary(out_dir: Path, stem: str,
351
+ *, system_config: str | None = None) -> dict[str, float]:
352
+ """Every `<mem_area>_memory_used` figure vela reported for this run, keyed
353
+ by lower-cased memory-area name ("sram", "dram", "on_chip_flash", ...), in
354
+ KiB.
355
+
356
+ The values are ALREADY KiB in the CSV, not bytes (`memory_used[...] /
357
+ 1024.0`, ethosu/vela/stats_writer.py:123 in
358
+ write_summary_metrics_csv_common) -- despite looking byte-scale at a glance
359
+ (e.g. `72.734375`). `arena_cache_size` (`arch.arena_cache_size / 1024`,
360
+ stats_writer.py:107) is a DIFFERENT column -- the accelerator config's
361
+ configured cache capacity, a build-time knob unrelated to any one model's
362
+ footprint -- and is deliberately not among these.
363
+
364
+ `{}` when the summary is missing or unparseable; a memory area vela did
365
+ not write is simply absent, and one it wrote as 0.0 is present as 0.0 (the
366
+ two are different facts, and `_refuse_zero_sram_footprint` says which)."""
367
+ path = _summary_csv(out_dir, stem, system_config)
368
+ if path is None:
369
+ return {}
370
+ with open(path, newline="", encoding="utf-8") as fh:
371
+ rows = list(csv.DictReader(fh))
372
+ if not rows:
373
+ return {}
374
+ used: dict[str, float] = {}
375
+ for key, val in rows[0].items():
376
+ m = _MEM_USED_RE.match(key.lower()) if key else None
377
+ if m is None:
378
+ continue
379
+ try:
380
+ used[m.group(1)] = float(val)
381
+ except (TypeError, ValueError):
382
+ continue
383
+ return used
384
+
385
+
386
+ def _footprint(used: dict[str, float]) -> tuple[int, int]:
387
+ """(arena_bytes, req_sram_kib) from @used's SRAM working set.
388
+
389
+ The model's arena requirement is its SRAM working set, `sram_memory_used`:
390
+ the scratch region alp-sdk's `alp_model_open()` `arena`/`arena_bytes` sizes
391
+ at runtime. req_sram_kib is rounded UP (ceil, never floor/truncate): the
392
+ device-side fit gate (`t->req_sram_kib <= e->arena_sram_kib`,
393
+ src/backends/inference/alp_model_select.c) must never under-report a
394
+ model's requirement, or a model that doesn't actually fit could pass the
395
+ gate as if it did.
396
+
397
+ (0, 0) only when vela reported no SRAM at all -- correct for a full CPU
398
+ fallback (measured: `float32_fc.tflite` at `ethos-u85-256` reports 0.0 for
399
+ EVERY memory area), and refused by the caller for anything else.
400
+
401
+ THE CONTRACT DECISION (tan-cli#1011), made once and recorded here rather
402
+ than re-litigated at the call site: under `--memory-mode Sram_Only` this
403
+ reads the ARENA ONLY, on purpose, and that is the CORRECT scope, not an
404
+ open gap. `e->arena_sram_kib` -- what `req_sram_kib` is checked against
405
+ (`t->req_sram_kib <= e->arena_sram_kib`, `alp_model_select.c`) -- is
406
+ sourced from the SoC spec's `inference_arena_sram_kib`, whose own schema
407
+ description (`metadata/schemas/soc-spec-v1.schema.json`) states its scope
408
+ verbatim: "Usable SRAM budget (KiB) for an NPU tensor arena on this SoC."
409
+ An ARENA budget, not a whole-model-footprint budget. And the const/weights
410
+ region this function deliberately leaves out is not sized by
411
+ `req_sram_kib` on the consumer side either: `alp_inference_open_alpmodel()`
412
+ hands it to the engine as `model_data`/`model_size`
413
+ (`src/common/alp_model_loader.c`), entirely separate from
414
+ `arena`/`arena_bytes` -- a `Sram_Only` integration provisions its storage
415
+ independently, the way `examples/aen/aen-npu-inference-alp/src/main.c`
416
+ sizes its own `model_sram[NETWORK_MODEL_LEN]
417
+ __attribute__((section("SRAM0")))` off the blob's own byte length, never
418
+ off `req_sram_kib`.
419
+
420
+ vela's own bookkeeping obscures this at a glance, which is why the
421
+ reasoning is recorded rather than assumed correct: it FILES the
422
+ const/weights region under `on_chip_flash_memory_used`, and on a
423
+ `Sram_Only` part that is a pure BOOKKEEPING RENAME rather than a placement
424
+ -- verbatim from `ethosu/vela/architecture_features.py`, when the const
425
+ area's port maps to SRAM and const/arena/cache are the same area: `"Info:
426
+ Changing const_mem_area from Sram to OnChipFlash. This will use the same
427
+ characteristics as Sram."`, after which `memory_clock_scales`,
428
+ `memory_burst_length`, `memory_ports_used` and `memory_latency` for
429
+ `OnChipFlash` are all assigned from `Sram`. It happens because
430
+ `[Memory_Mode.Sram_Only]` puts `const_mem_area`, `arena_mem_area` and
431
+ `cache_mem_area` all on `Axi0` (vela 5.1.0's own `vela.ini`) while vela's
432
+ validity check forbids naming SRAM as a const area.
433
+
434
+ On an Alif Ensemble module that renamed region really IS SRAM-resident too
435
+ (`src/backends/inference/ethos_u_aen.cpp` pins every NPU access to the
436
+ SRAM AXI port for exactly this reason -- verbatim, "everything a
437
+ SRAM_Only model touches is SRAM0-resident"), which is real information a
438
+ board integrator needs -- just not through THIS field. Measured with
439
+ `ethos-u-vela` 5.1.0 on the real 44-op `person_detect_int8.tflite` at
440
+ `ethos-u85-256`: `sram_memory_used = 72.0` (the arena; what `req_sram_kib`
441
+ reports) and `on_chip_flash_memory_used = 235.265625` (the const/weights
442
+ region; carried in the blob payload, sized by the integrator, never by
443
+ this function).
444
+
445
+ Summing the columns into `req_sram_kib` would be WRONG, not merely
446
+ unnecessary: both figures this function returns come from the same
447
+ `sram_kib` value, so summing would inflate `arena_bytes` right along with
448
+ it, oversizing the caller's scratch buffer for memory the model needs no
449
+ scratch space for, and it would double-book SRAM the const-storage buffer
450
+ already reserves on its own. And it is not even a `Sram_Only`-only wrong
451
+ answer: for an integration whose const region lands on a different AXI
452
+ port (`Shared_Sram` / `Dedicated_Sram*` -- see the module docstring's
453
+ `ethos-u65-256` sweep, where 228 KiB of weights moves between
454
+ `off_chip_flash` and `dram` on the system config alone), summing would
455
+ OVER-report `req_sram_kib` and refuse a board the model actually fits."""
456
+ sram_kib = used.get("sram", 0.0)
457
+ if sram_kib <= 0:
458
+ return 0, 0
459
+ return round(sram_kib * 1024), math.ceil(sram_kib)
460
+
461
+
462
+ def _defaulted_flags(stdout: str) -> frozenset[str]:
463
+ """Which profile flags vela fell back to a built-in for, by vela's OWN
464
+ names for them (`_SYSTEM_CONFIG_FLAG` / `_MEMORY_MODE_FLAG`), read from its
465
+ own warnings. Empty when it defaulted neither."""
466
+ return frozenset(_DEFAULTED_RE.findall(stdout))
467
+
468
+
469
+ def _profile_clause(system_config: str | None, memory_mode: str | None,
470
+ defaulted: frozenset[str]) -> str:
471
+ """The profile THIS run resolved, named as the run itself reported it
472
+ (`_parse_vela_profile`) -- never a hardcoded `Ethos_U85_*`, which would
473
+ blame an Alif memory model for a refusal on a non-Alif part
474
+ (tan-cli#789 review MAJOR 3).
475
+
476
+ @defaulted is vela's OWN verdict (`_defaulted_flags` over its stdout), not
477
+ an assumption, and it is per-flag: the blanket "vela's BUILT-IN default
478
+ profile" tail is used only when vela defaulted EVERY name in the clause.
479
+ When it defaulted some but not all -- the shape tan produces now that it
480
+ passes a metadata-sourced `--memory-mode` and no `--system-config` -- each
481
+ name says for itself which it was, so a supplied flag is never attributed
482
+ to vela and a defaulted one is never presented as the module's."""
483
+ parts = [(label, name) for label, name in
484
+ ((_SYSTEM_CONFIG_FLAG, system_config), (_MEMORY_MODE_FLAG, memory_mode)) if name]
485
+ if not parts:
486
+ return "an unreported profile"
487
+ named = " / ".join(name for _, name in parts)
488
+ if all(label in defaulted for label, _ in parts):
489
+ return f"{named}, vela's BUILT-IN default profile"
490
+ if not any(label in defaulted for label, _ in parts):
491
+ return named
492
+ return " / ".join(f"{name} (vela's built-in default)" if label in defaulted else name
493
+ for label, name in parts)
494
+
495
+
496
+ # THE VENDOR CLAUSE IN `_refusal_remedy` NAMES THE `System_Config`
497
+ # SPECIFICALLY, and must keep doing so (tan-cli#789 review MINOR 4). It used to
498
+ # read "it lives in the proprietary ensemble_vela.ini", whose antecedent is the
499
+ # "module vela profile" of the sentence before -- and since alp-sdk #1470 that
500
+ # is false for the load-bearing half of the profile: the memory mode
501
+ # (`Sram_Only` on every Alif Ensemble part) is an ARM BUILT-IN tan passes with
502
+ # no `.ini` at all. Only the tuned `System_Config` names (`Ethos_U85_SRAM_Only`,
503
+ # `RTSS_HE_SRAM_Only`) live in Alif's file. And since that branch is now reached
504
+ # only when the part's SoC spec publishes no `npu_toolchain.vela.memory_mode`,
505
+ # the reader's REAL fix is a metadata entry in alp-sdk, not a vendor download --
506
+ # the vendor clause survives only to explain why tan cannot name a complete
507
+ # profile there, never as the remedy. (Stating the metadata fix in the shipped
508
+ # string does not fit: `_VELA_REFUSAL_NOTE_BUDGET` is 700 and the maximal
509
+ # refusal already measures 691 with this clause in it --
510
+ # `tests/model/test_check.py::test_the_refusal_note_budget_covers_a_maximal_
511
+ # refusal` is what holds that line.)
512
+ #
513
+ # WHICH FILE IT NAMES IS NOW METADATA'S ANSWER, NOT A VENDOR PREFIX MATCH. The
514
+ # gate used to be `silicon_ref.startswith("alif:ensemble:")` with the filename
515
+ # `ensemble_vela.ini` hardcoded beside it -- a standing claim about one vendor
516
+ # that no data could correct. It is now emitted iff this part's own SoC spec
517
+ # declares `npu_toolchain.vela.vendor_config_filename`, and names exactly that.
518
+ # So "a non-Alif refusal never names an Alif file" holds for the stronger
519
+ # reason that no part is ever handed another part's file at all
520
+ # (a part whose spec declares none gets none). `silicon_ref` was the gate's only reader
521
+ # and is therefore gone from this adapter's interface.
522
+ #
523
+ # `System_Config` and NOT `--system-config`: that is vela's own INI section name
524
+ # (`Section System_Config.<name> not found in Vela config file`), so it
525
+ # identifies the right half of the profile without spelling a CLI flag into a
526
+ # message whose whole point is that it prescribes nothing tan can pass
527
+ # (tan-cli#789 review BLOCKER 2, pinned by
528
+ # `test_the_refusal_prescribes_nothing_tan_cannot_actually_do`).
529
+ #
530
+ # The MEMORY-MODE gate is also why that sentence no longer says tan "cannot pass
531
+ # one yet". It could not, when the text was written; since #1470 it does, for
532
+ # every part whose SoC spec publishes a `npu_toolchain.vela.memory_mode`.
533
+ # Reaching the branch now means THIS part's spec publishes none -- a part still
534
+ # marked TBD, which the sourcing rule leaves unset rather than guessing -- so the
535
+ # true statement is that none was resolved for it and vela therefore chose the
536
+ # placement itself.
537
+ def _refusal_remedy(defaulted: frozenset[str], vendor_config_filename: str | None) -> str:
538
+ """What the reader can actually DO -- which today is: nothing to this
539
+ target, and the rest of the SKU still builds.
540
+
541
+ The message this replaced ended "Compile against a --system-config/
542
+ --memory-mode matching this module's memory model instead", which
543
+ prescribed an action tan cannot perform (tan-cli#789 review BLOCKER 2):
544
+ `compile()` never reads `opts`, nothing under `tan/` passes either flag,
545
+ and alp-sdk's `board.schema.json` declares `models[].compile` as
546
+ `additionalProperties: false` over `deepx_dxm1`/`drpai` -- there is no
547
+ `ethos_u` key to route a profile through. Adding one is an alp-sdk schema
548
+ change (ADR-0028 leaves `metadata/schemas/` with alp-sdk), so this states
549
+ the real position instead of promising a flag that does not exist.
550
+
551
+ The vendor-file pointer is likewise stated only where it is TRUE
552
+ (tan-cli#789 review (g)). It was unconditional, so a refusal on a
553
+ part whose vela invocation involves no proprietary `.ini` sent that reader
554
+ hunting an Alif file. Gated now on @vendor_config_filename, this part's own
555
+ SoC-spec declaration, so `None` (the part declares none, or the caller
556
+ resolved no spec) gets the two clauses that hold for ALL parts and no
557
+ vendor file at all. The first sentence stays part-neutral by construction:
558
+ it says no profile was resolved for THIS part, which is a fact about that
559
+ part's metadata, not about anyone's silicon.
560
+
561
+ Gated on the MEMORY MODE specifically, not on "vela defaulted something":
562
+ the memory mode is the flag that decides placement, and since alp-sdk
563
+ #1470 tan passes it from SoC metadata while still passing no
564
+ `--system-config`. A run that got its module's memory mode and STILL
565
+ reported no SRAM has not been failed by a missing profile, so telling its
566
+ reader "no module vela profile was resolved" would be false. See the
567
+ comment block above this function for what that gate implies for the
568
+ vendor clause and for the retired "cannot pass one yet" wording."""
569
+ if _MEMORY_MODE_FLAG not in defaulted:
570
+ return "`tan model build` skips this target and still builds the SKU's others."
571
+ where = ""
572
+ if vendor_config_filename:
573
+ where = (f"; its System_Config lives in the proprietary {vendor_config_filename} "
574
+ f"alp-sdk does not redistribute")
575
+ return (f"No module vela profile was resolved for this part, so vela chose its "
576
+ f"own{where}. `tan model build` skips this target and still builds the "
577
+ f"SKU's others.")
578
+
579
+
580
+ def _no_dram_marker(area: str, soc_declares_dram: bool | None) -> str:
581
+ """`_NO_DRAM_MARKER` for the DRAM area on a part whose SoC spec declares no
582
+ DRAM interface; `""` for every other area and for every other answer.
583
+
584
+ `is False` and never a truthiness test: `None` means the spec declared no
585
+ `external_memory_interfaces` at all (`tan.model.targets._soc_declares_dram`),
586
+ and an unknown must not be rendered as evidence that a part has no DRAM.
587
+ The refusal is a string a customer sizes hardware from; the safe direction
588
+ is silence."""
589
+ if area == _DRAM_AREA and soc_declares_dram is False:
590
+ return _NO_DRAM_MARKER
591
+ return ""
592
+
593
+
594
+ def _refuse_zero_sram_footprint(*, accel_config: str, npu_ops: int, cpu_ops: int | None,
595
+ used: dict[str, float], system_config: str | None,
596
+ memory_mode: str | None, defaulted: frozenset[str],
597
+ vendor_config_filename: str | None = None,
598
+ soc_declares_dram: bool | None = None) -> NoReturn:
599
+ """A successful compile that placed operators on the NPU but reports no
600
+ SRAM working set is a refusal, not a zero.
601
+
602
+ `req_sram_kib == 0` does not read as "this model needs no arena" on the
603
+ device side: alp-sdk's selector reads it as *fits any envelope*
604
+ (`return e->arena_sram_kib == 0u || t->req_sram_kib <= e->arena_sram_kib;`,
605
+ src/backends/inference/alp_model_select.c) and then hands the caller
606
+ `arena_bytes = 0`. A board would trust that. Measured, real `ethos-u-vela`
607
+ 5.1.0: `keyword_scrambled_8bit.tflite` at `ethos-u85-256` places 6 of 15
608
+ operators on the NPU, exits 0, and reports `sram_memory_used = 0.0`
609
+ alongside `dram_memory_used = 5.359375` -- because vela's own default
610
+ profile is DRAM-backed. The footprint is real; it is just not expressed in
611
+ a memory area this module has. Refusing loudly is the only honest answer
612
+ available without inventing a profile (see the module docstring).
613
+
614
+ Raises `VelaFootprintRefused`, not a bare `RuntimeError`: this refusal
615
+ costs the caller ONE target, never the whole package (see that type).
616
+
617
+ THE EVIDENCE FOR "a memory area this module does not have" IS SOURCED, NOT
618
+ ASSERTED. @soc_declares_dram is this part's own SoC spec answering whether
619
+ it declares any external DRAM interface at all, resolved by
620
+ `tan.model.targets._soc_declares_dram` from `external_memory_interfaces[]`
621
+ and threaded here on `TargetSpec` -- so the clause is right for every part
622
+ automatically, including one nobody has looked at yet, and absent for a part
623
+ that does have DRAM. It is deliberately NOT resolved by reading a SoC JSON
624
+ from inside this adapter: a compiler adapter that opens `metadata/` would be
625
+ a second, unversioned reader of the SDK's fact tree, and the caller already
626
+ has the spec open."""
627
+ total = npu_ops + (cpu_ops or 0)
628
+ elsewhere = ", ".join(f"{area} {kib:.2f} KiB{_no_dram_marker(area, soc_declares_dram)}"
629
+ for area, kib in sorted(used.items()) if kib > 0)
630
+ where = (f"its working set went to {elsewhere}" if elsewhere
631
+ else "it reported no memory use in any area")
632
+ # NOTE the wording of the selector clause: it says "accepts ... against ANY
633
+ # arena size", NEVER the retired "fits" vocabulary. This string reaches a
634
+ # `basis: "static-screen"` report note through `tan.model.check`'s
635
+ # `_footprint_refused_note`, and a static screen must never emit "fits" in
636
+ # any form. Until this round the clause was there but the 200-character
637
+ # note budget happened to cut it off; widening that budget would have
638
+ # walked it straight into the envelope.
639
+ #
640
+ # THE ONE TEST THAT ENFORCES THAT is
641
+ # `test_a_refused_footprint_is_not_reported_as_a_failed_compile`
642
+ # (`tests/model/test_check.py`), and only because it renders its note by
643
+ # CALLING this function. It used to raise a hand-copied literal of this
644
+ # template, which enforced nothing: the copy was byte-identical to the
645
+ # template it came from, so rewording this clause to "reads req_sram_kib
646
+ # == 0 as fits any envelope" left the whole suite green while that phrase
647
+ # reached the customer note and the JSON envelope (tan-cli#789 review
648
+ # BLOCKER). The two static-screen guards this comment used to name --
649
+ # `test_the_word_fits_never_appears_in_a_static_screen_report`
650
+ # (`tests/model/test_analyze.py`) and `test_the_retired_word_never_leaks_
651
+ # into_the_clis_rendered_static_screen_output`
652
+ # (`tests/commands/test_model_check_command.py`) -- do NOT reach this
653
+ # template and never did: the first runs live reports that never provoke a
654
+ # refusal, and the second monkeypatches `check_model_backends` with
655
+ # fabricated reports whose notes are hand-written literals. Naming them
656
+ # here claimed an enforcement that did not exist, which is exactly what
657
+ # stops the next reader from adding a real one.
658
+ raise VelaFootprintRefused(
659
+ f"vela compiled cleanly for {accel_config} ({npu_ops}/{total} operators on the NPU) "
660
+ f"but reported 0 KiB SRAM: {where} under "
661
+ f"{_profile_clause(system_config, memory_mode, defaulted)}. Refused because alp-sdk's "
662
+ f"on-device selector accepts req_sram_kib == 0 against ANY arena size "
663
+ f"(src/backends/inference/alp_model_select.c). "
664
+ f"{_refusal_remedy(defaulted, vendor_config_filename)}")
665
+
666
+
667
+ # `_default_profile_caveats` EMITS THREE SHAPES, because the two profile flags
668
+ # carry different weight, since alp-sdk #1470 they routinely differ, and how
669
+ # much a DEFAULTED system config cost depends on the memory mode beside it:
670
+ #
671
+ # * NO MEMORY MODE -- the hard caveat, unchanged. vela chose the placement,
672
+ # its built-in default is DRAM-backed, and the arena/SRAM figures therefore
673
+ # describe a memory model the module may not have at all.
674
+ # * MEMORY MODE SUPPLIED WITH `Axi0` CONST (`Sram_Only`), SYSTEM CONFIG
675
+ # DEFAULTED -- the shape every Alif Ensemble compile takes. Here the default
676
+ # really is bandwidth-only: const, arena and cache are all on `Axi0` and all
677
+ # 11 Arm `System_Config` sections map `axi0_port=Sram`, so nothing the
678
+ # system config says can move a byte. Measured, `--memory-mode Sram_Only`
679
+ # alone versus `--system-config Ethos_U85_SYS_Flash_High --memory-mode
680
+ # Sram_Only`, on `tests/fixtures/models/tiny_int8.tflite` AND on the 44-op
681
+ # `person_detect_int8.tflite`, both at `ethos-u85-256`: identical
682
+ # `sram_memory_used`/`on_chip_flash_memory_used`. So this caveat does NOT
683
+ # repeat the "figures describe the wrong memory model" sentence, which would
684
+ # be false.
685
+ # * MEMORY MODE SUPPLIED WITH `Axi1` CONST (`Shared_Sram`; also every
686
+ # `Dedicated_Sram*`), SYSTEM CONFIG DEFAULTED --
687
+ # calling THAT bandwidth-only would be false, which is why it is its own
688
+ # shape (tan-cli#789 review MAJOR 2). The const/weights region is on the
689
+ # other port, and it is the system config that maps ports to memory areas,
690
+ # so vela's default picked where the weights live. Measured on
691
+ # `person_detect_int8.tflite` at `ethos-u65-256 --memory-mode Shared_Sram`,
692
+ # changing only `--system-config`: `Ethos_U65_Embedded` files 228.265625 KiB
693
+ # under `off_chip_flash`, `Ethos_U65_Mid_End` 228.3125 KiB under `dram`,
694
+ # `Ethos_U65_Client_Server` (vela's own default for this accel config)
695
+ # 228.25 KiB under `dram` -- same `sram 72.734375` throughout. This text
696
+ # ships inside the customer's `.alpmodel`, so it must say that placement was
697
+ # vela's, not the module's.
698
+ #
699
+ # A memory mode this module cannot classify (`_AXI0_CONST_MEMORY_MODES`) takes
700
+ # the third shape too -- under-claiming is the safe direction for a string a
701
+ # customer sizes hardware from. A run whose summary block named NO memory mode
702
+ # at all gets a fourth, narrower sentence: it says tan cannot tell which of the
703
+ # two happened, rather than picking one.
704
+ #
705
+ # `_default_profile_caveats` returns `()` -- no caveat at all -- ONLY when
706
+ # vela defaulted NEITHER flag, i.e. `compile()` passed a fully module-authored
707
+ # profile straight through. That does not mean the arena-only scope statement
708
+ # stops applying under `Sram_Only`: it is a CONTRACT about what `req_sram_kib`
709
+ # means, true whether or not vela had a flag to default (tan-cli#1021 review
710
+ # MINOR). `compile()` falls back to `_arena_only_scope_caveat` for exactly
711
+ # that case, so the sentence still reaches the fully-specified path -- the one
712
+ # issue #1011 named as live on every Alif Ensemble part.
713
+ def _arena_only_scope_clause(memory_mode: str) -> str:
714
+ """The `req_sram_kib`/arena-only contract sentence (tan-cli#1011), shared
715
+ verbatim between `_default_profile_caveats`' defaulted-profile branch and
716
+ `_arena_only_scope_caveat`'s standalone one -- so a fully-specified
717
+ compile and a defaulted one never carry two different spellings of the
718
+ same contract.
719
+
720
+ tan-cli#1021 review NIT: says only "provisioned separately", never
721
+ "reported ... separately" -- nothing in `tan` reports the const/weights
722
+ region's own size anywhere (`check.py`'s sizing note carries arena+SRAM
723
+ only, `manifest.Target` carries a blob index with no length, and
724
+ `model_cmd.py` prints no byte count for it); the blob PAYLOAD carries the
725
+ bytes, not a reported figure."""
726
+ return (f"That SRAM figure is the tensor arena only -- this module's const/weights region "
727
+ f"also lands in SRAM under {memory_mode} but is provisioned separately, never "
728
+ f"folded into it.")
729
+
730
+
731
+ def _arena_only_scope_caveat(memory_mode: str | None) -> tuple[str, ...]:
732
+ """The arena-only contract (tan-cli#1011), standalone -- for a compile
733
+ that used a FULLY module-authored profile, where `_default_profile_
734
+ caveats` returns `()` because vela had nothing left to default.
735
+
736
+ tan-cli#1021 review MINOR: the scope statement is a CONTRACT
737
+ (`req_sram_kib` always means the tensor arena under a `Sram_Only`-shaped
738
+ memory mode, whether or not vela also had to default a flag), not an
739
+ artefact of a defaulted profile -- so it must reach a fully-specified
740
+ `Sram_Only` compile too. That is the path issue #1011 named as live on
741
+ every Alif Ensemble part; gating the sentence behind `defaulted` being
742
+ non-empty left it unreachable there. `()` for any other memory mode --
743
+ only `_AXI0_CONST_MEMORY_MODES` carries this quirk (see the module
744
+ docstring's `_AXI0_CONST_MEMORY_MODES` note)."""
745
+ if memory_mode not in _AXI0_CONST_MEMORY_MODES:
746
+ return ()
747
+ return (_arena_only_scope_clause(memory_mode),)
748
+
749
+
750
+ def _default_profile_caveats(defaulted: frozenset[str], system_config: str | None,
751
+ memory_mode: str | None) -> tuple[str, ...]:
752
+ """vela's own "may be invalid or non-optimal" verdict, surfaced as a report
753
+ caveat for whichever profile flags it had to default -- so a customer
754
+ cannot mistake a default-profile figure for one authored for this module.
755
+ `()` when vela defaulted neither (the caller then falls back to
756
+ `_arena_only_scope_caveat` for the fully-specified case).
757
+
758
+ THREE SHAPES -- see the comment block above this function for what each
759
+ says and the measurements behind it.
760
+
761
+ Takes @defaulted rather than re-searching stdout: `compile()` resolves it
762
+ once (`_defaulted_flags`) and hands the SAME fact to this and to
763
+ `_refuse_zero_sram_footprint`, so the caveat and the refusal can never
764
+ disagree about which flags vela defaulted."""
765
+ if not defaulted:
766
+ return ()
767
+ named = ", ".join(f"{cli} {name}" for label, cli, name in
768
+ ((_SYSTEM_CONFIG_FLAG, "system-config", system_config),
769
+ (_MEMORY_MODE_FLAG, "memory-mode", memory_mode))
770
+ if name and label in defaulted)
771
+ if _MEMORY_MODE_FLAG in defaulted:
772
+ return (f"vela used its BUILT-IN default profile ({named or 'unreported'}), not one "
773
+ f"authored for this module -- vela's own warning for that is \"Compilation "
774
+ f"may be invalid or non-optimal\". The arena/SRAM figures and the compiled "
775
+ f"command stream describe that default memory model, not this module's.",)
776
+ default_of = named or "system-config unreported"
777
+ if memory_mode in _AXI0_CONST_MEMORY_MODES:
778
+ return (f"vela used its BUILT-IN default {default_of} for "
779
+ f"bandwidth/latency estimates -- no module-authored one is available -- so its "
780
+ f"scheduling is tuned for that system, not this module's. The arena/SRAM figures "
781
+ f"are unaffected: they follow --memory-mode {memory_mode}, which came from this "
782
+ f"module's SoC metadata, whose const/arena/cache areas are all one AXI port every "
783
+ f"system config maps to SRAM. " + _arena_only_scope_clause(memory_mode),)
784
+ if memory_mode is None:
785
+ return (f"vela used its BUILT-IN default {default_of} -- no module-authored one is "
786
+ f"available -- so its scheduling is tuned for that system, not this module's. "
787
+ f"This run reported no memory mode, so tan cannot say whether that default also "
788
+ f"chose the memory area the const/weights region landed in.",)
789
+ return (f"vela used its BUILT-IN default {default_of} -- no module-authored one is "
790
+ f"available -- so its scheduling is tuned for that system, not this module's, and "
791
+ f"under --memory-mode {memory_mode} it was NOT bandwidth-only: that mode puts the "
792
+ f"const/weights region on a different AXI port from the arena, and it is the system "
793
+ f"config that maps ports to memory areas, so vela's default also chose which memory "
794
+ f"the weights land in. Only the arena/SRAM figures follow --memory-mode "
795
+ f"{memory_mode}, which came from this module's SoC metadata.",)
796
+
797
+
798
+ def _parse_vela_placement(stdout: str) -> tuple[int, int] | None:
799
+ """(cpu_op_count, npu_op_count) from vela's own "CPU/NPU operators = N
800
+ (P%)" summary lines -- vela's REAL per-run placement verdict, not
801
+ inferred from its exit code (0 either way). None when either line is
802
+ absent (an unexpected vela output shape, e.g. a future version that
803
+ changes this text): a caller must not fabricate a placement it can't
804
+ actually read."""
805
+ counts = {k: int(v) for k, v in _PLACEMENT_RE.findall(stdout)}
806
+ if "CPU" not in counts or "NPU" not in counts:
807
+ return None
808
+ return counts["CPU"], counts["NPU"]
809
+
810
+
811
+ class VelaAdapter(CompilerAdapter):
812
+ backend = "ethos_u"
813
+
814
+ def is_available(self) -> bool:
815
+ return shutil.which("vela") is not None
816
+
817
+ def accepts(self, src_format: str) -> bool:
818
+ return src_format == "tflite"
819
+
820
+ def compile(self, source: Path, *, accel_config: str, out_dir: Path,
821
+ opts: dict | None = None,
822
+ vela_memory_mode: str | None = None,
823
+ vela_system_config: str | None = None,
824
+ vela_vendor_system_config: str | None = None,
825
+ vela_vendor_config_filename: str | None = None,
826
+ soc_declares_dram: bool | None = None) -> Blob:
827
+ # @vela_vendor_config_filename / @soc_declares_dram reach ONLY the
828
+ # refusal wording (`_refusal_remedy`, `_no_dram_marker`) -- the vela
829
+ # command line below is byte-identical with and without them, so no
830
+ # part can get a different artifact out of a diagnostic fact.
831
+ # @vela_memory_mode / @vela_system_config / @vela_vendor_system_config
832
+ # are the opposite: they are the SILICON's own profile
833
+ # (`TargetSpec.vela_*`, out of the SoC spec's `npu_toolchain.vela`) and
834
+ # they DO change the artifact -- that is the point. All stay optional
835
+ # and default to None, so a caller that resolved no profile gets
836
+ # byte-for-byte the flagless invocation this adapter has always issued.
837
+ run_dir = _run_dir(out_dir, accel_config)
838
+ run_dir.mkdir(parents=True, exist_ok=True)
839
+ cmd = ["vela", str(source), "--accelerator-config", accel_config,
840
+ "--output-dir", str(run_dir)]
841
+ # The memory mode is the load-bearing flag because it assigns const,
842
+ # arena and cache to AXI PORTS; the system config only maps those ports
843
+ # to memory areas. Under `Sram_Only` all three land on `Axi0` and every
844
+ # Arm `System_Config` section maps `axi0_port=Sram`, so the system
845
+ # config cannot move a byte -- measured, adding `--system-config
846
+ # Ethos_U85_SYS_Flash_High` alongside `--memory-mode Sram_Only` on
847
+ # `tiny_int8.tflite` and on `person_detect_int8.tflite` at
848
+ # `ethos-u85-256` changes no memory figure. That is a `Sram_Only` fact,
849
+ # NOT a general one: under `Shared_Sram` (`const_mem_area=Axi1`) the
850
+ # system config alone moves
851
+ # 228 KiB of weights between areas -- see the module docstring's
852
+ # measured `ethos-u65-256` sweep. Every memory-mode value that reaches
853
+ # here is an Arm built-in, so this works with no vendor .ini.
854
+ #
855
+ # SIDE EFFECT WORTH KNOWING, recorded not acted on: `Sram_Only` names
856
+ # no `arena_cache_size`, so vela schedules against an effectively
857
+ # unbounded SRAM budget. Measured on `person_detect_int8.tflite` at
858
+ # `ethos-u85-256`, vela's summary column moves `arena_cache_size`
859
+ # 384.0 -> 1073741824.0 and `total_npu_encoded_weights` 205472 ->
860
+ # 212096. Nothing in tan reads either column (`_parse_vela_summary`
861
+ # takes `*_memory_used` only, and tan-cli#789 stopped reading
862
+ # `arena_cache_size`), so this changes no shipped figure today.
863
+ if vela_memory_mode:
864
+ cmd += ["--memory-mode", vela_memory_mode]
865
+ # An ARM BUILT-IN system config is safe on its own -- `resolve_targets`
866
+ # only ever puts one here (`targets._vela_profile` withholds a
867
+ # vendor-gated name from this field), because a section vela cannot
868
+ # resolve is rc=1, not a degradation.
869
+ if vela_system_config:
870
+ cmd += ["--system-config", vela_system_config]
871
+ # THE VENDOR PROFILE: ALL THREE FLAGS OR NONE OF THEM. A licensed
872
+ # customer names their `.ini` in `ALP_VELA_CONFIG`, and only then does
873
+ # this part's vendor-gated `System_Config` become legal to pass.
874
+ #
875
+ # It is a TRIPLE, not the pair the design first assumed, and that is
876
+ # measured against real `ethos-u-vela` 5.1.0 rather than reasoned
877
+ # (`architecture_features.py:588-608`): supplying `--config` REPLACES
878
+ # vela's built-in `vela.ini` outright -- `self.vela_config_files =
879
+ # vela_config_files`, no merge -- and vela then refuses any default
880
+ # left beside it. Each of these was run:
881
+ #
882
+ # * `--config <ini>` alone -> rc=1, verbatim `Error: Incorrect
883
+ # argument to CLI option --config=['fake_vendor.ini']: Specifying a
884
+ # configuration file is not allowed when using a default system
885
+ # configuration`
886
+ # * `--config <ini> --system-config <name>` -> rc=1, `... Specifying a
887
+ # configuration file is not allowed when using a default memory
888
+ # mode`
889
+ # * the full triple, against an .ini defining BOTH sections -> rc=0,
890
+ # `System configuration Ethos_U85_SRAM_Only` / `Memory mode
891
+ # Sram_Only` in vela's own summary block
892
+ #
893
+ # so the vendor `.ini` must also define the `[Memory_Mode.<mode>]` this
894
+ # part declares -- Arm's built-in one is no longer being read. alp-sdk's
895
+ # own Alif recipe passes exactly this triple, and only
896
+ # `if(AEN_NPU_VELA_CONFIG)`
897
+ # (`examples/aen/aen-npu-inference-alp/CMakeLists.txt`).
898
+ #
899
+ # TODAY THIS BRANCH NEVER FIRES, and doing nothing is the correct
900
+ # behaviour rather than a gap: no SoC spec carries a `system_config` at
901
+ # all, because an Alif `System_Config` is per CORE SUBSYSTEM, not per
902
+ # SoC (one Ensemble die sources both `Ethos_U85_SRAM_Only` and
903
+ # `RTSS_HE_SRAM_Only`), so a per-SoC scalar would be right for one of
904
+ # its two or three Ethos-U accelerators and wrong for the rest. With
905
+ # `ALP_VELA_CONFIG` set and no vendor name resolved, passing `--config`
906
+ # anyway would be rc=1 and inventing a profile name would compile a
907
+ # command stream for hardware nobody described. The mechanism goes live
908
+ # the day metadata can name one per accelerator; until then it is wired
909
+ # and silent.
910
+ elif vela_memory_mode and vela_vendor_system_config:
911
+ vendor_config = _vendor_config_path()
912
+ if vendor_config is not None:
913
+ cmd += ["--config", str(vendor_config),
914
+ "--system-config", vela_vendor_system_config]
915
+ try:
916
+ proc = subprocess.run(
917
+ cmd, capture_output=True, text=True, timeout=_VELA_TIMEOUT_S, env=spawn_env()
918
+ )
919
+ except subprocess.TimeoutExpired as exc:
920
+ raise RuntimeError(f"vela timed out after {exc.timeout}s for {accel_config}") from exc
921
+ if proc.returncode != 0:
922
+ raise RuntimeError(f"vela failed for {accel_config}: {proc.stderr.strip()}")
923
+ produced = run_dir / f"{source.stem}_vela.tflite"
924
+ if not produced.is_file():
925
+ raise RuntimeError(f"vela produced no output at {produced}")
926
+ system_config, memory_mode = _parse_vela_profile(proc.stdout)
927
+ defaulted = _defaulted_flags(proc.stdout)
928
+ used = _parse_vela_summary(run_dir, source.stem, system_config=system_config)
929
+ arena, sram_kib = _footprint(used)
930
+ placement = _parse_vela_placement(proc.stdout)
931
+ cpu_ops, npu_ops = placement if placement is not None else (None, None)
932
+ # Only a CONFIRMED NPU placement refuses: 0 NPU operators is a real,
933
+ # legitimate 0 KiB footprint (full CPU fallback), and an unreadable
934
+ # placement summary already degrades through `tan.model.check`'s
935
+ # `_vela_placement_unreadable` rather than claiming anything.
936
+ if npu_ops and sram_kib <= 0:
937
+ _refuse_zero_sram_footprint(
938
+ accel_config=accel_config, npu_ops=npu_ops, cpu_ops=cpu_ops, used=used,
939
+ system_config=system_config, memory_mode=memory_mode, defaulted=defaulted,
940
+ vendor_config_filename=vela_vendor_config_filename,
941
+ soc_declares_dram=soc_declares_dram)
942
+ # tan-cli#1021 review MINOR: a FULLY module-authored profile (nothing
943
+ # defaulted) still gets the arena-only scope caveat under
944
+ # `_AXI0_CONST_MEMORY_MODES` -- `_default_profile_caveats` returns
945
+ # `()` there, since it exists to flag DEFAULTED flags only.
946
+ caveats = (_default_profile_caveats(defaulted, system_config, memory_mode)
947
+ or _arena_only_scope_caveat(memory_mode))
948
+ return Blob(format="vela_tflite", payload=produced.read_bytes(),
949
+ arena_bytes=arena, compiler_version=_vela_version(),
950
+ req_sram_kib=sram_kib, cpu_op_count=cpu_ops, npu_op_count=npu_ops,
951
+ caveats=caveats)