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,721 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Tier 2's MATCHING half: turn a customer's identity into the ONE
3
+ `metadata/model_perf/` point that describes their machine, and turn that point
4
+ into a `BackendReport`. `tan.model.perf` is deliberately only the READER (it
5
+ finds points and refuses to rank them, by its own module docstring); this is
6
+ the DECIDER, and `tan.model.check.check_model_backends` is its only caller.
7
+
8
+ Split out of `tan.model.check` (tan-cli#791 review NIT (c)): this is pure
9
+ string/decision logic with no subprocess in it -- `check.py` was 789 lines
10
+ against the house `module_cap: 800`, and `MODULE_SIZE_BUDGET_LOG.md`'s own
11
+ `ethos_u.py` entry already named exactly this shape of growth for a
12
+ neighbouring module ("the extraction to make when this file next grows").
13
+ This module takes the already-resolved Ethos-U target as a PARAMETER
14
+ (`target`) rather than resolving it itself (`_headline_ethos_u_target` stays
15
+ in `check.py`, which also needs it for `--exact`) -- passing it down avoids a
16
+ `check.py` <-> `perf_apply.py` import cycle, and it costs nothing: the caller
17
+ already has it in hand before either half of `check_model_backends`' per-model
18
+ work runs.
19
+
20
+ Everything below answers ONE question -- "did Alp Lab already measure THIS
21
+ model, on THIS module, at THIS accelerator target?" -- and does nothing at all
22
+ when the answer is no. That is not a courtesy, it is the contract: the same
23
+ absence rule `npu-ops-v1` states for a missing op table applies here, so a
24
+ customer whose model has never been benched (which is most of them) must get
25
+ exactly the report they get today, note for note. `apply_perf_point`
26
+ therefore returns @report ITSELF on every non-match path rather than a
27
+ rebuilt copy, with no exception: "unchanged" is structural rather than a
28
+ promise.
29
+
30
+ A point's `target.core` is one of the fields that non-match rule protects
31
+ (tan-cli#791 round-2 review item 1 / round-3's evidence-driven narrowing). The
32
+ die a SoC spec describes may pair a given NPU to a specific core, or may not --
33
+ alp-sdk's own schema says a shared SoC-level NPU legitimately declares no
34
+ `paired_core` at all -- and `_resolve_perf_point` enforces exactly that
35
+ DECLARED fact about the accelerator actually being screened, never a fact
36
+ borrowed from a sibling NPU of the same backend that happens to declare one.
37
+ See `_resolve_perf_point`'s own docstring for the sourced silicon evidence
38
+ (the E8's Ethos-U85) that makes the distinction concrete.
39
+
40
+ A BENCH POINT CANNOT CLAIM A COVERAGE VERDICT (tan-cli#1115). Reconciling
41
+ `tan.model.perf.PerfPoint` against alp-sdk's REAL `model-perf-v1` schema
42
+ removed `npu_ops`/`cpu_ops` -- fields the schema never actually carries (see
43
+ that module's docstring, "DEAD FIELDS") -- so this module can no longer ask
44
+ `coverage_from_placement` for a bench-derived `npu_coverage`. A matched point
45
+ overlays only the figures alp-sdk's schema actually publishes today:
46
+ `arena_bytes`, `req_sram_kib`, and latency once a capture campaign times one
47
+ (merged per field, most-specific-wins, against whatever @report already had --
48
+ `_merge_figure`, PR review blocker 3: a point that measured only latency must
49
+ not blank a real `--exact` compile's own `arena_bytes`/`req_sram_kib`). This
50
+ is a real, deliberate narrowing of what tier 2 claims versus an earlier,
51
+ unreconciled version of this module (which asked a fictional
52
+ `measured.npu_ops`/`cpu_ops` pair for a "fits"/"partial"/"cpu-only" verdict no
53
+ alp-sdk document has ever actually carried): the schema's own description of
54
+ `perf.latency_ms` says a capture campaign "can record fit/SRAM alone until a
55
+ timing harness exists", and there is likewise no wire representation of an
56
+ op-by-op NPU/CPU split for `apply_perf_point` to have been reading in the
57
+ first place.
58
+
59
+ COVERAGE ITSELF IS WITHHELD, NOT INHERITED, UNLESS @report ALREADY CARRIES A
60
+ REAL PLACEMENT (PR review blocker 1 -- an earlier version of this PR
61
+ inherited `npu_coverage`/`ops`/`uncosted_cpu_op_count`/`npu_placement_
62
+ pct_real` from @report UNCONDITIONALLY, which silently promoted the static
63
+ screen's name-level, capped-positive ESTIMATE -- `docs/model-check-static-
64
+ screen.md`'s own vocabulary calls `partial`/`full-eligible` at
65
+ `basis: "static-screen"` explicitly "Not a placement claim" -- into a
66
+ `basis: "bench", confidence: "certain"` report, exactly the estimate-wearing-
67
+ a-measurement's-clothes failure this module's `compute_on_npu_pct_max`
68
+ handling already refuses two paragraphs down in `_perf_point_report`). The
69
+ rule: those four fields carry forward from @report ONLY when `report.basis ==
70
+ "compiled"` (a real, `--exact`-measured placement, safe to keep under
71
+ `confidence: "certain"`); otherwise the rebased report states
72
+ `npu_coverage: "undetermined"`, `ops: []`, `uncosted_cpu_op_count: 0`,
73
+ `npu_placement_pct_real: None` -- the SRAM/arena/latency figures are still
74
+ certain measurements, the coverage word honestly is not, and
75
+ `docs/model-check-static-screen.md` documents this exact combination as
76
+ legitimate. The one way `npu_coverage` still reaches `"fits"` at `basis:
77
+ "bench"` is DECISION 1's agreement path below: the customer's own `--exact`
78
+ compile already measured `"fits"` for real, the bench point corroborates it
79
+ on arena/SRAM, and the rebased report inherits that real, `--exact`-measured
80
+ verdict rather than a bench-authored one.
81
+ """
82
+ from __future__ import annotations
83
+
84
+ import hashlib
85
+ from dataclasses import replace
86
+ from pathlib import Path
87
+
88
+ from .analyze import COVERAGE_WITHHELD, BackendReport, OpVerdict
89
+ from .perf import PerfPoint, coverage_from_placement, find_perf_points
90
+ from .targets import TargetSpec
91
+
92
+
93
+ def _placement_outcome(report: BackendReport, npu_ops: int | None,
94
+ cpu_ops: int | None) -> tuple[str, list[OpVerdict], int] | None:
95
+ """(npu_coverage, ops, uncosted_cpu_op_count) for a MEASURED placement, or
96
+ `None` when there is no readable placement at all.
97
+
98
+ 100% NPU is the only case that reports `"fits"`, and it drops `ops`: no
99
+ per-op verdict can add anything to "every operator ran on the NPU".
100
+ Partial or 0% KEEPS @report's static per-op verdicts AND its
101
+ uncosted-cpu-op count -- a real partial/0% result must erase neither
102
+ (MINOR review).
103
+
104
+ Takes the two COUNTS rather than a `Blob`: today's ONLY caller is
105
+ `tan.model.check._report_from_vela_compile`, off a real vela compile's own
106
+ "CPU/NPU operators = N" summary. It is NOT called for a bench perf point
107
+ any more (tan-cli#1115) -- alp-sdk's real schema carries no per-operator
108
+ placement counts on a published point, so there is nothing to feed this
109
+ with on that path; see this module's own docstring, "A BENCH POINT CANNOT
110
+ CLAIM A COVERAGE VERDICT". The coverage word itself comes from
111
+ `tan.model.perf.coverage_from_placement` -- the one function in tan
112
+ allowed to return `"fits"`."""
113
+ coverage = coverage_from_placement(npu_ops, cpu_ops)
114
+ if coverage is None:
115
+ return None
116
+ if coverage == "fits":
117
+ return coverage, [], 0
118
+ return coverage, report.ops, report.uncosted_cpu_op_count
119
+
120
+
121
+ #: backend -> the leading token of the REAL `target.compiler_version` string
122
+ #: that backend's compile adapter actually writes into a `.alpmodel` manifest
123
+ #: (and, by the same vocabulary, into a bench point -- `model-perf-v1.schema.
124
+ #: json`'s own description pins `compiler_version` to that manifest field).
125
+ #: Measured directly against alp-sdk's producers, not the op-support-table
126
+ #: filename vocabulary (`npu_ops/drpai/onnx-i8@translator-1.12.json`'s
127
+ #: `translator` is a DIFFERENT namespace -- a table-authoring convention, not
128
+ #: a compiler-identity string -- and an earlier version of this dict
129
+ #: conflated the two, matching NEITHER of `drpai`/`deepx_dxm1`'s real points:
130
+ #: tan-cli#1115 PR review, the issue's own failure mode one level down):
131
+ #:
132
+ #: ethos_u `_vela_version()` "vela {version}" / "vela"
133
+ #: (ethos_u.py:21-24)
134
+ #: drpai `_compiler_version()` "drp-ai_tvm {version}" / "drp-ai_tvm"
135
+ #: (drpai.py:96-110)
136
+ #: deepx_dxm1 `_dxcom_version()` "DX-COM {version}" / "dxcom" (bare,
137
+ #: (deepx.py:38-45) differently-spelled fallback -- see
138
+ #: `tan.model.perf._toolchain_name`'s
139
+ #: own docstring for why that spelling
140
+ #: is not chased)
141
+ #:
142
+ #: Compared case-foldedly by `find_perf_points` (alp-sdk's three real
143
+ #: producers do not agree on case -- `vela`/`drp-ai_tvm` lowercase, `DX-COM`
144
+ #: upper -- and case is not part of a measurement's identity), so this dict's
145
+ #: own values only need to be A correctly-spelled casing, not the specific
146
+ #: one a given point happens to carry; `test_check.py`'s
147
+ #: `test_perf_toolchain_matches_the_real_producer_strings` pins all three
148
+ #: against these cited literal format strings.
149
+ #:
150
+ #: `cpu` is deliberately ABSENT rather than mapped to a made-up name: a
151
+ #: backend with no entry here is simply never asked for a point, which is the
152
+ #: right behaviour for one nobody benches. `cpu` STAYS absent on purpose
153
+ #: (tan-cli#791 review, item 6): the schema's `target.backend` enum includes
154
+ #: it, so alp-sdk COULD publish a CPU-baseline campaign, but
155
+ #: `resolve_check_backends` (`tan.model.check`) already excludes `cpu` from
156
+ #: what `tan model check` screens at all -- "how much of this model can
157
+ #: target the NPU" has nothing to say about the fallback CPU path itself, and
158
+ #: that decision predates tier 2. Wiring `cpu` in here without also
159
+ #: revisiting that exclusion would publish points this reader can never be
160
+ #: asked for, which is worse than not wiring it: a campaign planner who greps
161
+ #: this dict for "which backends does tier 2 read" gets a true, complete
162
+ #: answer as of THIS decision, made here rather than rediscovered from a
163
+ #: silent no-op.
164
+ _PERF_TOOLCHAIN = {"ethos_u": "vela", "drpai": "drp-ai_tvm", "deepx_dxm1": "DX-COM"}
165
+
166
+
167
+ def _model_sha256(source: Path) -> str:
168
+ """Lowercase sha256 of @source's exact bytes -- the perf-point match key.
169
+
170
+ Chunked, because a real model is not a small file (a production ONNX runs
171
+ to tens of MB), and computed ONCE per `check` rather than once per backend.
172
+ Any `OSError` propagates exactly as `extract_ops`' own read of the same file
173
+ does: an unreadable source is `model.check-failed`, never a quietly degraded
174
+ report."""
175
+ digest = hashlib.sha256()
176
+ with source.open("rb") as handle:
177
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
178
+ digest.update(chunk)
179
+ return digest.hexdigest()
180
+
181
+
182
+ def _resolve_hw_rev(sku: str, metadata_root: Path, declared: str | None) -> str | None:
183
+ """The module revision a perf point must have been measured on: the
184
+ customer's own `board.yaml` `som.hw_rev` when they declared one, else the
185
+ SKU preset's `default_hw_rev`.
186
+
187
+ That fallback is the SDK's own rule, not an invention -- `board.schema.json`
188
+ says of `som.hw_rev`: "Optional; the SDK falls back to the SKU preset's
189
+ `default_hw_rev` when omitted" -- and both spellings use the same `rN` keys
190
+ the perf-point schema requires, so no translation is involved.
191
+
192
+ `None` when neither answers, and `find_perf_points` matches NOTHING on a
193
+ `None`: an r2 measurement handed to a customer holding an r1 module is
194
+ exactly the "exactly measured, describes a different machine" failure that
195
+ put `hw_rev` in a point's identity in the first place (alp-sdk `f724d3e4`).
196
+ Soft-fail on a preset that does not exist or does not parse, matching
197
+ `resolve_ethos_u_variant`'s stance next door.
198
+
199
+ tan-cli#1116: NOT a pre-flight `preset_path.is_file()` -- that used to
200
+ gate the read below, and on Python <= 3.13 `Path.is_file()` RAISES for a
201
+ permission-denied ancestor -- `PermissionError`, `errno=13`, measured on
202
+ both 3.12.3 and 3.13.15 -- swallowing only `ENOENT`/`ENOTDIR`/`EBADF`/
203
+ `ELOOP` there. `pathlib`'s `_IGNORED_ERRNOS` CONSTANT that names those
204
+ four is itself gone a release earlier (3.13, `AttributeError` probing
205
+ for it), but the raising BEHAVIOUR it used to name outlives the constant
206
+ by one release and only actually changes in 3.14: there `is_file()`
207
+ swallows EVERY `OSError`, `EACCES` included, and returns `False`
208
+ instead (tan-cli#1116 review round 2 -- a pre-flight guard is dead on
209
+ 3.14+ regardless of what it catches, and reasoning from either "the
210
+ constant still exists" or "I measured the raise" picks the wrong
211
+ boundary, since the two diverge for exactly the 3.13 release). NOT
212
+ `EACCES`, so a
213
+ preset whose CONTAINING directory the caller cannot traverse raised a raw
214
+ `PermissionError` right there, past this function's own "Soft-fail"
215
+ contract -- the exact `document_guards.read_catalog_document` trap ("not
216
+ a pre-flight `is_file()`"), just via `is_file()` instead of
217
+ `Path.exists()`. Reading straight through the `try` below answers the
218
+ same three cases (absent, a directory, no permission) through `OSError`
219
+ alone -- on EVERY supported interpreter, since it never calls `is_file()`
220
+ at all -- so the pre-flight added nothing a plain `FileNotFoundError`
221
+ from the read did not already give it. `UnicodeDecodeError` is also now
222
+ caught explicitly: it is a `ValueError`, not an `OSError` and not a
223
+ `yaml.YAMLError`, so a non-UTF-8 preset used to escape raw past this same
224
+ contract (measured, tan-cli#1116).
225
+
226
+ `declared` reaching here as `None` is itself a FAIL-CLOSED answer from the
227
+ caller, not a "customer said nothing" default: `tan.commands.model_cmd.
228
+ _declared_hw_rev` refuses the whole run outright (a `model.board-yaml-
229
+ invalid` issue) when `som.hw_rev` is present but not a usable string --
230
+ e.g. an unquoted YAML int (`hw_rev: 2`) -- rather than letting it fall
231
+ through to here and silently pick up the preset's `default_hw_rev`
232
+ instead (tan-cli#791 review MINOR 5: measured `{'som': {'hw_rev': 2}} ->
233
+ None` serving an r2 preset measurement to a customer who wrote r2 but
234
+ would have been served r1's if the preset defaulted there)."""
235
+ if declared:
236
+ return declared
237
+ preset_path = Path(metadata_root) / "e1m_modules" / f"{sku}.yaml"
238
+ import yaml # noqa: PLC0415 -- deferred (tan-cli#810); see sdk_cmd's `_releases_opener`
239
+
240
+ try:
241
+ preset = yaml.safe_load(preset_path.read_text(encoding="utf-8")) or {}
242
+ except (OSError, UnicodeDecodeError, yaml.YAMLError):
243
+ return None
244
+ value = preset.get("default_hw_rev") if isinstance(preset, dict) else None
245
+ return str(value) if value else None
246
+
247
+
248
+ def _part_declares_a_profile(target: TargetSpec) -> bool:
249
+ """Does the SoC spec that produced @target name a vela memory profile at
250
+ all -- ANY of `vela_memory_mode` / `vela_system_config` /
251
+ `vela_vendor_system_config`?
252
+
253
+ Mirrors `_profile_matches_the_part`'s own `stated` computation exactly,
254
+ because the two must agree: a part this says `True` about is one
255
+ `_resolve_perf_point` (below) now narrows on BEFORE the single-point
256
+ shortcut, and a part this says `False` about must not be narrowed on at
257
+ all -- narrowing unconditionally would turn "the part is silent" into
258
+ "reject every point", which is the opposite failure from the BLOCKER this
259
+ function exists to close (tan-cli#791 review item 1): a part that states
260
+ nothing has nothing for `_profile_matches_the_part` to ever agree with, so
261
+ an unconditional narrow would empty out even the correctly-profiled
262
+ ordinary case."""
263
+ system_config = target.vela_system_config or target.vela_vendor_system_config
264
+ return bool(target.vela_memory_mode) or bool(system_config)
265
+
266
+
267
+ def _profile_matches_the_part(point: PerfPoint, target: TargetSpec) -> bool:
268
+ """Was @point captured under the memory profile THIS part declares?
269
+
270
+ The tiebreak, and the only one applied: the SoC spec's own
271
+ `npu_toolchain.vela` block (`TargetSpec.vela_memory_mode` /
272
+ `vela_system_config` / `vela_vendor_system_config`) is what `tan model
273
+ build` compiles this part under, so a point captured under the same profile
274
+ is the one describing THIS module. Comparing on a silicon fact rather than
275
+ on recency or on file order is the whole difference between choosing and
276
+ guessing -- the collision that put the profile into a point's identity left
277
+ a DRAM-backed capture as the arbitrary survivor on a part with no DRAM.
278
+
279
+ A field the SoC spec does not declare is not compared: `system_config` is
280
+ withheld from `TargetSpec` for every part shipped today (the Alif names live
281
+ only in a proprietary `.ini`), so on those parts this narrows on the memory
282
+ mode alone. `False` whenever there is nothing to compare at all -- an
283
+ unnarrowable ambiguity must fall through, not resolve to "everything
284
+ matches". CALLERS must gate this on `_part_declares_a_profile` first
285
+ (above) when using it to REJECT rather than to TIEBREAK: this function
286
+ alone cannot distinguish "nothing declared" from "declared and disagrees",
287
+ and only the caller knows which failure mode it is currently guarding."""
288
+ system_config = target.vela_system_config or target.vela_vendor_system_config
289
+ checks = [(target.vela_memory_mode, point.vela_memory_mode),
290
+ (system_config, point.vela_system_config)]
291
+ stated = [(declared, measured) for declared, measured in checks if declared]
292
+ return bool(stated) and all(declared == measured for declared, measured in stated)
293
+
294
+
295
+ def _topology_core_ids(sku: str, metadata_root: Path) -> set[str]:
296
+ """Every core id @sku's OWN SoM preset `topology:` map declares -- the
297
+ physical cores this module actually has, keyed exactly as a perf point's
298
+ `target.core` must be (`metadata/schemas/model-perf-v1.schema.json`'s own
299
+ `target.core` description: "The SoM `topology:` role ... that issued this
300
+ measurement -- the core running the inference call, whether or not it
301
+ also owns the NPU doing the compute").
302
+
303
+ LEVEL 1 of the core check (tan-cli#791 round-2 review item 1). Applied
304
+ UNCONDITIONALLY, for every backend, regardless of whether the exact-match
305
+ query (`_resolve_perf_point`, below) already narrowed `core` to the
306
+ accelerator's own declared `paired_core`: a point naming a core the die
307
+ does not physically have is wrong either way. This is the check the
308
+ BLOCKER fix (the profile tiebreak, `_part_declares_a_profile`) left
309
+ undone, and it does not depend on any accelerator declaring a pairing at
310
+ all -- unlike the query's own narrowing, which only ever fires for an
311
+ accelerator that names one. Measured through `tan model check` (the
312
+ production path) on a part whose accelerator declares no `paired_core`: a
313
+ point claiming `core: "m55_hp"` -- a core that part does not have -- was
314
+ consumed at `basis: "bench", confidence: "certain"` with no check ever
315
+ having looked at `core` at all, because there was no `paired_core` for the
316
+ query to narrow on either.
317
+ This function is what makes that check possible for EVERY backend and
318
+ EVERY SKU, not only the ones that happen to pair an NPU to a core: a
319
+ die's `topology:` is required and always populated (all eleven shipped
320
+ presets declare one), so `p.core in _topology_core_ids(...)` is always a
321
+ real, checkable fact -- unlike `paired_core`, which is genuinely absent
322
+ for some accelerators by design.
323
+
324
+ `UnicodeDecodeError` is caught explicitly (tan-cli#1116, measured
325
+ escaping before this fix): it is a `ValueError`, not an `OSError` and
326
+ not a `yaml.YAMLError`, so a non-UTF-8 preset used to raise raw past
327
+ this function's own "refuses [to] `set()`" contract, same shape as
328
+ `_resolve_hw_rev`'s sibling fix next door.
329
+
330
+ By the time this runs, @sku's preset is already known to exist and parse
331
+ (`resolve_check_backends` -> `resolve_targets` succeeded before a single
332
+ backend was screened, reading the SAME file), so this re-reads it rather
333
+ than threading a parsed doc all the way down here, matching
334
+ `_resolve_hw_rev`'s own stance next door. `set()` -- which REFUSES every
335
+ point, never admits one -- only for a preset that dropped `topology:`
336
+ entirely or left it malformed, which no shipped SoM does: a core this
337
+ function cannot vouch for is not proof the SKU has it, the same "absent
338
+ means refuse" reading `find_perf_points` already gives a `None` `hw_rev`
339
+ (`PerfPoint.hw_rev` is REQUIRED and non-empty, so it can never equal an
340
+ unresolvable query value either)."""
341
+ preset_path = Path(metadata_root) / "e1m_modules" / f"{sku}.yaml"
342
+ import yaml # noqa: PLC0415 -- deferred (tan-cli#810); see sdk_cmd's `_releases_opener`
343
+
344
+ try:
345
+ preset = yaml.safe_load(preset_path.read_text(encoding="utf-8")) or {}
346
+ except (OSError, UnicodeDecodeError, yaml.YAMLError):
347
+ return set()
348
+ topology = preset.get("topology") if isinstance(preset, dict) else None
349
+ return set(topology) if isinstance(topology, dict) else set()
350
+
351
+
352
+ def _one_line(text: str) -> str:
353
+ """@text with every run of whitespace collapsed to a single space.
354
+
355
+ Applied to every FOREIGN string a perf-point note quotes (the capture
356
+ citation, the toolchain version, the hardware revision): a note is one line
357
+ in both the text report and the JSON envelope by contract, and these come
358
+ out of a document in ANOTHER repository that this reader deliberately does
359
+ not schema-validate."""
360
+ return " ".join(text.split())
361
+
362
+
363
+ def _perf_measured_clauses(point: PerfPoint) -> list[str]:
364
+ """The `perf` figures @point actually carries, as note clauses.
365
+
366
+ A figure the point omits produces NO clause -- it was not measured, and
367
+ the note must not imply a zero. `latency_runs` travels with the latency
368
+ for the same reason the schema requires it: a latency without a run count
369
+ is a single shot wearing a measurement's clothes. NO operator-placement
370
+ clause: alp-sdk's real schema carries no per-op NPU/CPU split (tan-cli
371
+ #1115) -- see this module's own docstring, "A BENCH POINT CANNOT CLAIM A
372
+ COVERAGE VERDICT" -- so there is nothing sourced to phrase one from."""
373
+ clauses = []
374
+ if point.arena_bytes is not None:
375
+ clauses.append(f"arena {point.arena_bytes} bytes")
376
+ if point.req_sram_kib is not None:
377
+ clauses.append(f"{point.req_sram_kib} KiB resident SRAM")
378
+ if point.latency_ms_mean is not None and point.latency_runs is not None:
379
+ latency = f"{point.latency_ms_mean} ms mean"
380
+ if point.latency_ms_p95 is not None:
381
+ latency += f", {point.latency_ms_p95} ms p95"
382
+ clauses.append(f"{latency} over {point.latency_runs} runs")
383
+ return clauses
384
+
385
+
386
+ def _perf_identity(point: PerfPoint) -> str:
387
+ """The measurement identity, as one readable clause -- module, hardware
388
+ revision, driving core, accelerator target, the exact compiler build, and
389
+ the memory profile the figures describe. Every one of them is in the note
390
+ because every one of them CHANGES THE NUMBER (alp-sdk `f724d3e4`), and a
391
+ customer sizing hardware has to be able to see which machine was measured.
392
+
393
+ The CORE is the one the match rule may leave unnarrowed -- where the
394
+ SPECIFIC accelerator being screened declares no `paired_core` of its own
395
+ (every drpai/deepx_dxm1 SoM; and the
396
+ E8's Ethos-U85, genuinely shared silicon rather than core-private -- see
397
+ `_resolve_perf_point`) -- so stating it here is the only way the customer
398
+ learns which one this point actually describes. `compiler_version` is
399
+ always stated verbatim (e.g. `"vela 4.1.0"`): the tier-2 customer has no
400
+ local toolchain to compare it against, so the exact build this point was
401
+ captured under is the only fact they get. The PROFILE is there for the
402
+ reason the schema makes it required on an Ethos-U point at all: an arena
403
+ captured under a DRAM-backed profile is exactly measured and describes the
404
+ wrong machine, and hiding it would re-open on the reading side the
405
+ ambiguity the schema closed on the writing side.
406
+
407
+ THE PROFILE CLAUSE IS `ethos_u`-ONLY (tan-cli#791 round-2 review item 5).
408
+ `vela_system_config`/`vela_memory_mode` are only ever narrowed or
409
+ verified against silicon for `ethos_u` -- `_part_declares_a_profile`/
410
+ `_profile_matches_the_part` run only when `target is not None`, and
411
+ `target` is `None` for every OTHER backend by construction
412
+ (`tan.model.check.check_model_backends` only resolves one for
413
+ `backend == "ethos_u"`). Nothing sourced exists to check a drpai/
414
+ deepx_dxm1 point's profile fields against, which is a DEFENSIBLE gap --
415
+ there is no vela-shaped profile concept on those toolchains to check --
416
+ but printing whatever those two fields happen to hold as if it were this
417
+ point's verified memory profile is not: measured, a drpai point carrying
418
+ a stray Ethos-U85 `Ethos_U85_SYS_DRAM_Mid`/`Dedicated_Sram_384KB` pair
419
+ printed that nonsense verbatim into the customer-facing note. So this
420
+ clause is withheld outright for every backend but `ethos_u`, regardless
421
+ of what the point's own fields carry."""
422
+ target = point.accel_config or point.backend
423
+ profile = ("" if point.backend != "ethos_u" else
424
+ ", ".join(_one_line(p) for p in
425
+ (point.vela_system_config, point.vela_memory_mode) if p))
426
+ return (f"{point.sku} {_one_line(point.hw_rev)} (core {_one_line(point.core)}) "
427
+ f"at {_one_line(target)} with {_one_line(point.compiler_version)}"
428
+ + (f" ({profile})" if profile else ""))
429
+
430
+
431
+ def _perf_point_note(point: PerfPoint, *, corroborated: bool) -> str:
432
+ """The one-line note a `basis: "bench"` report leads with.
433
+
434
+ Carries `capture.bench_id` verbatim (whitespace-collapsed) -- WHICH
435
+ physical bench/rig captured this point (e.g. `"e1m-aen-evk-01"`), not a
436
+ capture artefact's own citation (that mislabel, "raw capture", used to be
437
+ the note's own wording -- tan-cli#1115 PR review minor) -- because a
438
+ number nobody can trace back to a specific rig is not reproducible, and a
439
+ perf point is worth exactly what its reproducibility is. Closes by saying
440
+ that a local compile can differ -- true for every backend, unlike an
441
+ `--exact` suggestion, which would be wrong advice on the two
442
+ license-gated ones."""
443
+ tail = ("This host's own compile agrees with it." if corroborated else
444
+ "A compile on your own host can differ if its toolchain profile "
445
+ "differs from the one recorded here.")
446
+ return (f"bench: Alp Lab measured this exact model on {_perf_identity(point)} "
447
+ f"-- {'; '.join(_perf_measured_clauses(point))}. Captured "
448
+ f"{_one_line(point.capture_date)} on bench "
449
+ f"{_one_line(point.capture_bench_id)}. {tail}")
450
+
451
+
452
+ def _perf_disagreements(point: PerfPoint, report: BackendReport) -> list[str]:
453
+ """Every concrete way @point and the customer's OWN `basis: "compiled"`
454
+ @report differ -- empty when they agree on everything both measured.
455
+
456
+ "Disagrees" is defined on the two figures BOTH sides really produce: the
457
+ tensor arena and the resident SRAM. NOT the placement verdict any more
458
+ (tan-cli#1115): alp-sdk's real schema carries no bench-measured
459
+ operator-placement split for a bench point to disagree about (see this
460
+ module's own docstring, "A BENCH POINT CANNOT CLAIM A COVERAGE VERDICT")
461
+ -- `report.npu_coverage` is a real `--exact` compile's OWN verdict either
462
+ way, so there is nothing left to compare it against. Latency has no
463
+ compiled counterpart to disagree with either, and a figure EITHER side
464
+ did not report is never a disagreement: absence is not evidence here any
465
+ more than anywhere else in this tier."""
466
+ diffs: list[str] = []
467
+ for label, unit, measured, compiled in (
468
+ ("arena", "bytes", point.arena_bytes, report.arena_bytes),
469
+ ("SRAM", "KiB", point.req_sram_kib, report.req_sram_kib)):
470
+ if measured is not None and compiled is not None and measured != compiled:
471
+ diffs.append(f"{label} (bench {measured} {unit}, local compile {compiled} {unit})")
472
+ return diffs
473
+
474
+
475
+ def _perf_disagreement_note(point: PerfPoint, diffs: list[str]) -> str:
476
+ """Why the customer's own compile is being reported and OUR measurement is
477
+ not -- with the measurement still named, so nothing is hidden."""
478
+ return (f"--exact wins here: Alp Lab's bench point for this exact model on "
479
+ f"{_perf_identity(point)} ({_one_line(point.capture_bench_id)}) "
480
+ f"disagrees with this host's own compile -- {'; '.join(diffs)}. The "
481
+ f"LOCAL compile is what is reported above: it ran this host's own "
482
+ f"toolchain and profile, which is what a build here would produce.")
483
+
484
+
485
+ #: A note whose text starts with `"--exact"` is a diagnostic about THIS RUN's
486
+ #: HOST/toolchain environment for the customer's `--exact` request (vela
487
+ #: absent, license-gated, footprint refused, ...) -- every HOST-fact
488
+ #: note-authoring site in `tan.model.check`'s exact-compile path shares this
489
+ #: prefix by construction (`_license_gated_exact_note`, `_footprint_refused_
490
+ #: note`, and every degrade branch in `_exact_ethos_u`/`_vela_placement_
491
+ #: unreadable`). The BASIS-clause sites in that SAME path deliberately do
492
+ #: NOT share it -- "This stays the static screen."/"Reporting the static
493
+ #: screen instead." (`check.py:233`, `:341`, `:387`, `:429`, `:518`) -- and
494
+ #: that is by design, not an oversight: `_perf_point_report` (below) uses the
495
+ #: prefix to tell the two kinds of note apart. The former survives a re-base
496
+ #: onto a bench point because it is still true of this run; the latter does
497
+ #: not, because it describes a basis the report no longer has (tan-cli#791
498
+ #: review item 4: measured the "--exact was requested, but vela is not on
499
+ #: PATH" note present with no perf point and GONE the moment one matched).
500
+ _EXACT_HOST_NOTE_PREFIX = "--exact"
501
+
502
+
503
+ def _merge_figure(point_value, report_value):
504
+ """THE MERGE RULE for every footprint/latency figure a rebase touches
505
+ (tan-cli#1115 PR review blocker 3): @point's own value when it has one,
506
+ else whatever @report already carried -- MOST-SPECIFIC-WINS, per field,
507
+ never a blanket "the point's block replaces the report's".
508
+
509
+ A matched bench point is not guaranteed to carry every figure (the
510
+ schema's `perf.latency_ms` is optional even where `req_sram_kib`/
511
+ `arena_bytes` are schema-required, and this reader stays defensive
512
+ against a malformed point missing even those -- see `tan.model.perf`'s
513
+ own `test_a_missing_or_malformed_perf_block_still_reads_the_identity`),
514
+ so a rebase that unconditionally overwrote every figure with @point's
515
+ (`None` included) blanked a REAL `--exact` compile's own measured
516
+ `arenaBytes`/`reqSramKib` the moment a matched point recorded only
517
+ latency -- reproduced: a compiled report's `arena_bytes=123456,
518
+ req_sram_kib=121` became `None, None` under `basis: "bench", confidence:
519
+ "certain"`, directly contradicting `apply_perf_point`'s own promise that
520
+ an under-informative point "cannot degrade a report by even a note".
521
+ Applied uniformly to every figure below, not only the two that were
522
+ reproduced: a bench point's latency is likewise never allowed to blank a
523
+ real compile's (today impossible in practice -- `_report_from_vela_
524
+ compile` never sets latency -- but the rule is stated once, for every
525
+ field, rather than fixed only at the two sites a repro happened to hit."""
526
+ return point_value if point_value is not None else report_value
527
+
528
+
529
+ def _perf_point_report(report: BackendReport, point: PerfPoint) -> BackendReport:
530
+ """@report re-based on @point: `basis: "bench"`, `confidence: "certain"`.
531
+
532
+ Drops @report's BASIS-describing notes and leads with the bench note,
533
+ exactly as `tan.model.check._report_from_vela_compile` does for a real
534
+ compile -- those notes describe a basis this report no longer has. Notes
535
+ that instead describe the HOST/toolchain environment for a `--exact`
536
+ request (`_EXACT_HOST_NOTE_PREFIX`) are still true of this run and are
537
+ KEPT, appended after the bench note (tan-cli#791 review item 4).
538
+
539
+ Two rules, both spelled out in full in this module's own docstring
540
+ ("A BENCH POINT CANNOT CLAIM A COVERAGE VERDICT" -- read that first):
541
+ footprint/latency figures merge PER FIELD, most-specific-wins
542
+ (`_merge_figure`, PR review blocker 3); coverage/placement fields carry
543
+ from @report ONLY when `report.basis == "compiled"`, else `npu_coverage`
544
+ is withheld to `"undetermined"` (PR review blocker 1). `compute_on_npu_
545
+ pct_max` is FORCED to `None` regardless -- `analyze.BackendReport`'s own
546
+ field comment: that MAC-weighted figure is "ONLY at basis: static-screen",
547
+ same reason it is already `None` at `basis: "compiled"`.
548
+
549
+ `sram_fit` (tan-cli#1288) rides the SAME `real_placement` gate as
550
+ `ops`/`npu_placement_pct_real`: it is a verdict about the COMPILED blob a
551
+ real `--exact` run just produced, not something a bare bench point can
552
+ independently corroborate (alp-sdk's `model_perf/` schema carries no
553
+ SRAM0-residency figure of its own, tan-cli#1115's "DEAD FIELDS"), so it
554
+ only survives the rebase when this report was ALREADY `basis: "compiled"`
555
+ -- otherwise it is dropped to `None`, exactly like `ops`'s own
556
+ withhold-when-not-corroborated shape."""
557
+ host_notes = [n for n in report.notes if n.startswith(_EXACT_HOST_NOTE_PREFIX)]
558
+ real_placement = report.basis == "compiled"
559
+ return BackendReport(
560
+ backend=report.backend, variant=report.variant, table=report.table,
561
+ npu_coverage=report.npu_coverage if real_placement else COVERAGE_WITHHELD,
562
+ compute_on_npu_pct_max=None,
563
+ npu_placement_pct_real=report.npu_placement_pct_real if real_placement else None,
564
+ uncosted_cpu_op_count=report.uncosted_cpu_op_count if real_placement else 0,
565
+ ops=report.ops if real_placement else [],
566
+ basis="bench", confidence="certain",
567
+ arena_bytes=_merge_figure(point.arena_bytes, report.arena_bytes),
568
+ req_sram_kib=_merge_figure(point.req_sram_kib, report.req_sram_kib),
569
+ latency_ms_mean=_merge_figure(point.latency_ms_mean, report.latency_ms_mean),
570
+ latency_ms_p95=_merge_figure(point.latency_ms_p95, report.latency_ms_p95),
571
+ latency_runs=_merge_figure(point.latency_runs, report.latency_runs),
572
+ perf_ref=point.capture_bench_id,
573
+ notes=[_perf_point_note(point, corroborated=report.basis == "compiled"), *host_notes],
574
+ sram_fit=report.sram_fit if real_placement else None,
575
+ )
576
+
577
+
578
+ def _resolve_perf_point(*, backend: str, sku: str, model_sha256: str,
579
+ hw_rev: str | None, metadata_root: Path,
580
+ target: TargetSpec | None) -> PerfPoint | None:
581
+ """The ONE point that describes this customer's module, or `None`.
582
+
583
+ @target is the ALREADY-RESOLVED Ethos-U accelerator
584
+ (`tan.model.check._headline_ethos_u_target`'s answer for @backend ==
585
+ `"ethos_u"`, `None` for every other backend) -- passed in rather than
586
+ resolved here so this module never has to import `check.py` back.
587
+
588
+ Narrows on everything tan can SOURCE: the SKU, the module revision, the
589
+ accel config `--exact` would compile for, the model's own bytes, and the
590
+ toolchain name this backend's points are keyed by. It does NOT narrow on
591
+ the toolchain VERSION -- there is no local toolchain to read one off in the
592
+ tier-2 premise, and a point measured at another version is still OUR
593
+ measurement (decision 1).
594
+
595
+ CORE narrows in TWO WAYS, and both are DECLARED facts -- never an
596
+ inference borrowed from elsewhere on the die (tan-cli#791 round-3,
597
+ driven by newly-sourced silicon evidence; see below). The exact-match
598
+ query below narrows to @target's OWN `paired_core` when @target declares
599
+ one -- the case for every Ethos-U55 published today, including both of
600
+ the E8's (`m55_hp`/`m55_he`): that pairing is a fact about the SAME
601
+ accelerator being screened, so enforcing it is sound. LEVEL 1
602
+ (`_topology_core_ids`), applied UNCONDITIONALLY for every backend
603
+ regardless of what @target declares: a point's core must exist in @sku's
604
+ OWN `topology:` map at all, full stop -- this is what catches a point
605
+ claiming `core: "m55_hp"` on a SoM whose topology has no such core
606
+ or any other core nothing on the module
607
+ has ever had.
608
+
609
+ WHERE @target DECLARES NO `paired_core` OF ITS OWN, core narrowing STOPS
610
+ at level 1 -- tan infers NOTHING further, and in particular never borrows
611
+ a SIBLING NPU's pairing on the same die. A prior round of this reader did
612
+ exactly that: it took the UNION of `paired_core` across every NPU of
613
+ @backend on the die and applied it even to an accelerator that named no
614
+ pairing of its own, on the reasoning that the E8's Ethos-U85 (unpaired)
615
+ shares a die with two Ethos-U55s (paired to `m55_hp`/`m55_he`), so a point
616
+ naming `a32_cluster` for the U85 "must" be wrong. Newly-sourced silicon
617
+ evidence proves that inference wrong: the U85's `NPU_HG_BASE` register
618
+ alias is byte-identical across BOTH M55 cores' generated headers, its
619
+ vendor DFP `<feature>` element carries no `Pname` at all (unlike the two
620
+ Ethos-U55s' `Pname="M55_HP"`/`"M55_HE"`), and it is clock-gated
621
+ SYSTEM-side (`SHMEM_CLK_CTRL.ZAPHOD_CKOR`) rather than from either core's
622
+ own config block -- it is genuinely shared SoC-level silicon, not a
623
+ core-private accelerator that merely forgot to declare its pairing. alp-
624
+ sdk's own `soc-spec-v1.schema.json` says exactly that is legitimate:
625
+ `paired_core`'s own field description says to "Omit for a shared /
626
+ non-core-paired NPU". A point naming ANY core @sku's own topology admits
627
+ (level 1) is therefore accepted for such an accelerator, `a32_cluster`
628
+ included -- accepting it is not a claim that the U85 CAN be driven from
629
+ the A32 cluster, only that the customer's own report of what they ran is
630
+ not contradicted by any sourced fact. Nothing sourced says it cannot, either:
631
+ there is no A32 SVD view and no A32 board devicetree NPU node to check
632
+ either way, so refusing the core would have been an equally unsourced
633
+ assertion in the OTHER direction. Only where @target declares no pairing
634
+ at all -- e.g. every drpai/deepx_dxm1 SoM today -- does level 1 alone
635
+ decide, exactly as it always did.
636
+
637
+ THE PROFILE TIEBREAK NARROWS BEFORE THE SINGLE-POINT SHORTCUT, not after
638
+ (tan-cli#791 review item 1, the BLOCKER). A part that declares a vela
639
+ memory profile (`_part_declares_a_profile`) only ever matches a point
640
+ captured under that SAME profile -- a lone point captured under a profile
641
+ the part does NOT declare (a DRAM-backed default measured against a
642
+ Sram_Only part) must be refused exactly as decisively as a second,
643
+ wrongly-profiled point in a multi-point tie already was: the old code
644
+ applied this ONLY once two or more points survived the sourced filters
645
+ above, so a single wrong-profile point was returned unfiltered at
646
+ `basis: "bench", confidence: "certain"` -- overstating resident SRAM by
647
+ 5.3x in the measured case (a DRAM-backed default capture against
648
+ E8-class e8.json's `Sram_Only`-declaring part). Applied only where the
649
+ part actually states a profile: narrowing on a part that states nothing
650
+ would reject every point outright, the opposite failure.
651
+
652
+ THE MULTI-MATCH RULE, for what is left. More than one point can
653
+ legitimately still stand -- one model, one module, one core, one
654
+ toolchain, captured under two different vela profiles the PART itself
655
+ leaves unconstrained (`_part_declares_a_profile` false for it). Nothing
656
+ further is picked: still several, or none once narrowed, falls through to
657
+ the next fidelity tier, silently, rather than guessing."""
658
+ toolchain = _PERF_TOOLCHAIN.get(backend)
659
+ if toolchain is None:
660
+ return None
661
+ if backend == "ethos_u" and target is None:
662
+ return None
663
+ points = find_perf_points(
664
+ sku=sku, backend=backend, accel_config=target.accel_config if target else "",
665
+ model_sha256=model_sha256, toolchain=toolchain, hw_rev=hw_rev,
666
+ core=target.paired_core if target else None, metadata_root=metadata_root)
667
+ topology_cores = _topology_core_ids(sku, metadata_root)
668
+ points = [p for p in points if p.core in topology_cores]
669
+ if target is not None and _part_declares_a_profile(target):
670
+ points = [p for p in points if _profile_matches_the_part(p, target)]
671
+ return points[0] if len(points) == 1 else None
672
+
673
+
674
+ def apply_perf_point(report: BackendReport, *, backend: str, sku: str,
675
+ model_sha256: str, hw_rev: str | None,
676
+ metadata_root: Path, target: TargetSpec | None) -> BackendReport:
677
+ """Tier 2, or @report untouched.
678
+
679
+ Every early return hands back the SAME object, with no exception: an
680
+ absent, ambiguous, fixture-marked, or empty point cannot degrade a report
681
+ by even a note. The last of those is the subtle one: a point that carries
682
+ none of `arena_bytes`/`req_sram_kib`/`latency_ms_mean` -- schema-legal but
683
+ with nothing left for this function to actually contribute -- must not
684
+ re-base the report to `basis: "bench"` over an empty note; that would
685
+ claim a measurement said something it didn't.
686
+
687
+ A MATCHED POINT NEVER DETERMINES `npu_coverage` ITSELF -- see this
688
+ module's own docstring ("A BENCH POINT CANNOT CLAIM A COVERAGE VERDICT")
689
+ and `_perf_point_report`'s for the full withhold-unless-`"compiled"` rule.
690
+
691
+ DECISION 1 (tier-2 plan, Task 4): a `basis: "compiled"` report -- the
692
+ customer really does hold the toolchain and really did compile -- WINS
693
+ over a disagreeing point, named in a note instead; their profile may not
694
+ be ours, and the number they can act on is the one their machine
695
+ produced. THE POINT'S OWN LATENCY AND CAPTURE CITATION STILL RIDE AS
696
+ FIELDS on that disagreement report (tan-cli#791 review item 3): a compile
697
+ has no wall-clock or raw-capture counterpart to disagree WITH, so they
698
+ are never part of `_perf_disagreements`' comparison, only offered
699
+ alongside the winning LOCAL numbers -- without this `perfRef` was `null`
700
+ in the same envelope whose own note prose quoted it.
701
+
702
+ When the two AGREE the point wins, because it adds measured wall-clock
703
+ latency and a traceable capture no compile can produce, and the local
704
+ compile corroborating it is said out loud."""
705
+ point = _resolve_perf_point(backend=backend, sku=sku, model_sha256=model_sha256,
706
+ hw_rev=hw_rev, metadata_root=metadata_root, target=target)
707
+ if point is None:
708
+ return report
709
+ if (point.arena_bytes is None and point.req_sram_kib is None
710
+ and point.latency_ms_mean is None):
711
+ return report
712
+ if report.basis == "compiled":
713
+ diffs = _perf_disagreements(point, report)
714
+ if diffs:
715
+ return replace(
716
+ report, notes=[*report.notes, _perf_disagreement_note(point, diffs)],
717
+ perf_ref=point.capture_bench_id,
718
+ latency_ms_mean=point.latency_ms_mean,
719
+ latency_ms_p95=point.latency_ms_p95,
720
+ latency_runs=point.latency_runs)
721
+ return _perf_point_report(report, point)