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.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1514 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan model build`/`tan model doctor`/`tan model check`/`tan model list` --
|
|
3
|
+
compile + package `board.yaml`'s `models:` block into `.alpmodel` packages,
|
|
4
|
+
report NPU-compiler toolchain availability, statically screen a declared
|
|
5
|
+
model's NPU eligibility against the SoM's own support tables, list what
|
|
6
|
+
is declared next to what is already built, and browse/adopt the SDK's model
|
|
7
|
+
zoo (`zoo`, `add`).
|
|
8
|
+
|
|
9
|
+
Port of `scripts/alp_cli/model.py` (51 lines): the board.yaml discovery,
|
|
10
|
+
per-model source/compile-option path resolution, and the `built <path>`
|
|
11
|
+
summary all move here, in-process, exactly as they read there. Also
|
|
12
|
+
in-process, since ADR-0028: `tan.model.build.build_model` itself -- the
|
|
13
|
+
compiler-adapter engine (CPU/Vela/DRP-AI/DeepX) that does the actual work,
|
|
14
|
+
relocated verbatim from alp-sdk's `scripts/alp_model/` into `tan.model`. This
|
|
15
|
+
command calls it directly, per model, no subprocess involved.
|
|
16
|
+
|
|
17
|
+
Earlier revisions of this file spawned a `python -c` driver under the SDK
|
|
18
|
+
checkout's own interpreter, on the premise that the vendor NPU-compiler
|
|
19
|
+
tooling was only reachable from that checkout's Python environment. That
|
|
20
|
+
premise was never accurate: every adapter resolves its own tool with
|
|
21
|
+
`shutil.which` and spawns an external binary -- `adapters/ethos_u.py`
|
|
22
|
+
(`shutil.which("vela")`, `cmd = ["vela", ...]`), `adapters/deepx.py`
|
|
23
|
+
(`shutil.which("dxcom")`, `cmd = ["dxcom", "-m", ...]`), `adapters/drpai.py`
|
|
24
|
+
likewise -- so what building a model actually needs is `vela`/`dxcom` on
|
|
25
|
+
PATH, a host fact, not a checkout fact. The one genuine Python-environment
|
|
26
|
+
dependency, `adapters/ethos_u.py`'s `_vela_version()`, already degrades
|
|
27
|
+
gracefully (`except PackageNotFoundError: return "vela"`), costing a less
|
|
28
|
+
precise `compiler_version` string and nothing else. **This module still
|
|
29
|
+
resolves the SDK root** (via `resolve_metadata_sdk_root`) -- not for its
|
|
30
|
+
Python, but because `tan.model.build.build_model` reads alp-sdk's
|
|
31
|
+
`metadata/**` at call time (ADR-0017: metadata stays in alp-sdk).
|
|
32
|
+
|
|
33
|
+
This is a REAL implementation, not a forward: it never spawns `python -m
|
|
34
|
+
alp_cli`, so `alp_cli` stops being load-bearing for `tan model` (the point of
|
|
35
|
+
this port -- see `crates/tan-cli/src/commands/sdk_cli.rs`'s module doc for
|
|
36
|
+
what it is replacing).
|
|
37
|
+
|
|
38
|
+
**Deliberate divergence 1 from the oracle survives ADR-0028**: `alp_cli/
|
|
39
|
+
model.py` has no try/except around `build_model()` at all, so a build failure
|
|
40
|
+
(e.g. "no blob compiled for model") tracebacks the whole click command. Every
|
|
41
|
+
command in this port instead resolves to a coded issue, never a traceback
|
|
42
|
+
(the established rule -- see `generate_cmd`'s module doc) -- so a per-model
|
|
43
|
+
failure here is caught around the direct `build_model()` call and reported as
|
|
44
|
+
a `model.build-failed` issue, and the run continues to the next model rather
|
|
45
|
+
than aborting the whole batch.
|
|
46
|
+
|
|
47
|
+
**Deliberate divergence 2 from the oracle is RETIRED with the driver it
|
|
48
|
+
existed for.** It used to guard against a spawned driver that exited 0 having
|
|
49
|
+
silently produced no result for a declared model -- a failure mode only a
|
|
50
|
+
subprocess boundary could produce. With `build_model()` called directly,
|
|
51
|
+
in-process, per model, there is no boundary across which a result can go
|
|
52
|
+
missing: either the call returns a path (recorded as built) or raises
|
|
53
|
+
(caught and reported as `model.build-failed`). There is no third outcome left
|
|
54
|
+
to guard against, so the guard is gone, not silently dropped.
|
|
55
|
+
|
|
56
|
+
**`doctor` is the diagnostic half ADR-0028 makes tan responsible for.** With
|
|
57
|
+
the compiler-adapter engine relocated here, tan is the customer's only
|
|
58
|
+
surface for "why did my compile produce nothing" -- `model doctor` answers
|
|
59
|
+
that ahead of a build by reporting, per registered backend, whether its
|
|
60
|
+
toolchain is installed and, if not, an ACTIONABLE reason (`tan.core.
|
|
61
|
+
model_doctor`). It is READ-ONLY: every adapter's `is_available()` is a
|
|
62
|
+
`shutil.which`/env-var check today, never a spawn, and `_run_doctor` below
|
|
63
|
+
never calls one that isn't. `_run_doctor` does not always take that
|
|
64
|
+
`is_available()` verdict verbatim, though: `deepx_dxm1` and `drpai` each get
|
|
65
|
+
a NARROWER doctor-side probe (`_deepx_dxm1_status`/`_drpai_status` below)
|
|
66
|
+
that checks what `compile()` actually reads/spawns, because each adapter's
|
|
67
|
+
`is_available()` ORs in a signal `compile()` never acts on -- reporting a row
|
|
68
|
+
green there meant the very next `model build` failed anyway. Still read-only,
|
|
69
|
+
still never a spawn. An unavailable toolchain is the expected, common
|
|
70
|
+
case, not a failure -- `_run_doctor` always resolves to `ExitCode.SUCCESS`;
|
|
71
|
+
reporting absence is the feature, not a reason to fail the run. A board.yaml
|
|
72
|
+
and a resolvable alp-sdk checkout are NOT required: this command intends to
|
|
73
|
+
run on the exact host where something else already broke, so an unresolved
|
|
74
|
+
`--sdk-root` is folded into a `model.doctor-sdk-unresolved` WARNING rather
|
|
75
|
+
than the `model.sdk-root-unresolved` ERROR `build` uses for the same
|
|
76
|
+
situation -- the backend rows below it are unaffected by a missing SDK either
|
|
77
|
+
way, since none of them reads `metadata/**`.
|
|
78
|
+
|
|
79
|
+
`doctor` also reports OPTIONAL prerequisites, in `data.optional[]` and in the
|
|
80
|
+
same five-key row shape -- today one: the vendor vela config `.ini` a licensed
|
|
81
|
+
customer names in `ALP_VELA_CONFIG` (`_vela_vendor_config_status`). Kept out of
|
|
82
|
+
`backends[]` deliberately: `available: false` there means tan cannot compile
|
|
83
|
+
for that backend, while an absent vendor `.ini` means the backend works and an
|
|
84
|
+
enhancement is not installed. Without it vela uses Arm's own built-in system
|
|
85
|
+
config, which is what the arena/SRAM figures tan reports already describe, so
|
|
86
|
+
neither the JSON nor the text line may read as a fault.
|
|
87
|
+
|
|
88
|
+
**`check` is the static NPU-eligibility screen ADR-0028's amendment adds
|
|
89
|
+
(tan-cli#782).** `tan.model.check`/`tan.model.analyze` do the actual work
|
|
90
|
+
(resolving @sku's real NPU backends, walking a model's ops, scoring them);
|
|
91
|
+
this module only resolves the board.yaml the same way `build` does, calls
|
|
92
|
+
that engine per declared model, and shapes the envelope. `check`'s exit code
|
|
93
|
+
is ALWAYS `SUCCESS` for a run that completed -- `partial`/`cpu-only`/
|
|
94
|
+
`undetermined` are the feature this command exists to report, never a
|
|
95
|
+
failure. See `tan.model.check`'s own module doc for `--exact`.
|
|
96
|
+
|
|
97
|
+
**`list` is the smallest of the four (tan-cli#674): what `board.yaml`
|
|
98
|
+
declares, next to what `--out` already holds for each one.** It mirrors
|
|
99
|
+
`doctor`'s SDK-root TOLERANCE, not `build`'s `_require_metadata_sdk_root`
|
|
100
|
+
refusal -- naming what is declared and what is already built on disk needs no
|
|
101
|
+
`metadata/**` at all, so `_run_list` itself never resolves, refuses, or warns
|
|
102
|
+
over a *metadata* SDK root the way `_run_build`/`_run_check`/`_run_doctor` do
|
|
103
|
+
(no `--sdk-root`/`--metadata-root` handling inside it, no `model.sdk-root-
|
|
104
|
+
unresolved`/`model.doctor-sdk-unresolved`).
|
|
105
|
+
|
|
106
|
+
**That is narrower than "never resolves or warns about an SDK root" (tan-cli#674
|
|
107
|
+
review MAJOR 2)** -- `list` still goes through the SAME shared project-context
|
|
108
|
+
preamble every subcommand here does (`resolve_project_context`, below), and
|
|
109
|
+
that preamble's own resolution is what populates the envelope's `sdk` block
|
|
110
|
+
and what `sdk_resolution_issues` reads to warn about a broken `.alp/sdk-path`
|
|
111
|
+
pin or a foreign global default. Measured: `tan model list --format json` from
|
|
112
|
+
a workspace with a resolvable alp-sdk checkout emits a real `"sdk":{"root":
|
|
113
|
+
...,"sourceTier":...}` block, and `test_a_broken_project_pin_is_reported_on_a_
|
|
114
|
+
model_list` (`tests/commands/test_model_list_command.py`) already pins a
|
|
115
|
+
`sdk.project-pin-unresolved` warning on this exact subcommand. `list` opts out
|
|
116
|
+
of only the SECOND, metadata-reading resolution `build`/`check`/`doctor`
|
|
117
|
+
additionally perform -- not the first.
|
|
118
|
+
|
|
119
|
+
It is read-only and spawns nothing: a declared model's `.alpmodel`
|
|
120
|
+
(`tan.model.build.build_model`'s own `{name}.alpmodel` naming) is only ever
|
|
121
|
+
`stat()`-ed and its manifest read back, never compiled. See
|
|
122
|
+
`tan.core.model_list`'s own module doc for the per-model `artifact` shape,
|
|
123
|
+
including `stale` (has `source` changed since the package on disk was built)
|
|
124
|
+
and the `model.artifact-stale-unknown` warning a readback failure there emits.
|
|
125
|
+
|
|
126
|
+
**`zoo` and `add` (tan-cli#1286)** read `<sdk>/metadata/model_zoo/` -- alp-sdk
|
|
127
|
+
owns that data, tan the engine (`tan.core.model_zoo`). Their runners and text
|
|
128
|
+
renderers live in `tan.commands.model_zoo_cmd` (this file is over its size
|
|
129
|
+
budget); both need a resolvable SDK (`model.sdk-root-unresolved`, as `build`/
|
|
130
|
+
`check`), and `add` additionally reads `board.yaml` and appends to its
|
|
131
|
+
`models:` with a comment-preserving text splice (`tan.core.board_yaml_edit`).
|
|
132
|
+
|
|
133
|
+
**`prep` (tan-cli#1287, host tier)** INT8-quantizes an ONNX model against a
|
|
134
|
+
calibration directory and reports the fp32-vs-int8 accuracy delta; it needs the
|
|
135
|
+
optional `model` extra (`tan.core.model_host`) and no SDK, board.yaml or
|
|
136
|
+
hardware. Runner and renderer: `tan.commands.model_host_cmd`.
|
|
137
|
+
|
|
138
|
+
**`run --device` / `ab --device` (tan-cli#1287, on-device tier)** parse a benchmark
|
|
139
|
+
app's console into the same envelope with `tier: device`
|
|
140
|
+
(`tan.commands.model_device_cmd`): from `--capture FILE`, or live -- without
|
|
141
|
+
`--capture` the already-built `diagnostics.link: itcm` project is RAM-run through
|
|
142
|
+
`tan flash --ram`'s Flow C (`--confirm`, never MRAM) and its `ram_console_buf`
|
|
143
|
+
parsed (`tan.commands.model_device_live`, rows carry `source: live`).
|
|
144
|
+
|
|
145
|
+
**`run` / `ab` (tan-cli#1287, host tier)** time an ONNX model on onnxruntime CPU
|
|
146
|
+
(`backend: cpu-host`) and compare two models -- host references, never SoM
|
|
147
|
+
performance; the on-device tier is bench-gated and not yet ported.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
from __future__ import annotations
|
|
151
|
+
|
|
152
|
+
import os
|
|
153
|
+
import shutil
|
|
154
|
+
import sys
|
|
155
|
+
from pathlib import Path
|
|
156
|
+
from typing import Any
|
|
157
|
+
|
|
158
|
+
import typer
|
|
159
|
+
|
|
160
|
+
from tan.commands.build_output import (
|
|
161
|
+
ProjectContext,
|
|
162
|
+
resolve_metadata_sdk_root,
|
|
163
|
+
resolve_project_context,
|
|
164
|
+
)
|
|
165
|
+
from tan.commands.model_zoo_cmd import (
|
|
166
|
+
add_empty_data,
|
|
167
|
+
render_add_text,
|
|
168
|
+
render_zoo_text,
|
|
169
|
+
run_add,
|
|
170
|
+
run_zoo,
|
|
171
|
+
zoo_empty_data,
|
|
172
|
+
)
|
|
173
|
+
from tan.commands.model_device_cmd import run_device_ab, run_device_run
|
|
174
|
+
from tan.commands.model_device_live import LiveOptions
|
|
175
|
+
from tan.commands.model_host_cmd import (
|
|
176
|
+
ab_empty_data,
|
|
177
|
+
prep_empty_data,
|
|
178
|
+
render_ab_text,
|
|
179
|
+
render_prep_text,
|
|
180
|
+
render_run_text,
|
|
181
|
+
run_ab,
|
|
182
|
+
run_empty_data,
|
|
183
|
+
run_prep,
|
|
184
|
+
run_run,
|
|
185
|
+
)
|
|
186
|
+
from tan.commands.sdk_cmd import NO_SDK_NEXT_STEPS
|
|
187
|
+
from tan.core.sdk_discovery import with_sdk_search
|
|
188
|
+
from tan.core.global_flags import accept_global_flags
|
|
189
|
+
from tan.core.model_check import backend_report_as_dict, render_check_text
|
|
190
|
+
from tan.core.model_doctor import backend_row, optional_row, registry_backends
|
|
191
|
+
from tan.core.model_list import declared_sku, list_entry, render_list_text
|
|
192
|
+
from tan.core.sdk_discovery import sdk_resolution_issues
|
|
193
|
+
from tan.core.shapes import rejected_sdk_root_message
|
|
194
|
+
from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
|
|
195
|
+
from tan.exit_codes import ExitCode
|
|
196
|
+
from tan.model.adapters.drpai import _compiler_version as _drpai_compiler_version
|
|
197
|
+
from tan.model.adapters.drpai import _tvm_home as _drpai_tvm_home
|
|
198
|
+
from tan.model.adapters.ethos_u import _VELA_CONFIG_ENV, _vela_version, _vendor_config_path
|
|
199
|
+
from tan.model.build import SramNoFitRefused, _ADAPTERS, _NAME_RE, build_model
|
|
200
|
+
from tan.model.check import check_model_backends, resolve_check_backends
|
|
201
|
+
from tan.model.package import read_manifest_file
|
|
202
|
+
from tan.output_format import FORMAT_HELP, OutputFormat
|
|
203
|
+
|
|
204
|
+
#: `data.schemaVersion` for `build`'s payload.
|
|
205
|
+
DATA_SCHEMA_VERSION = "1"
|
|
206
|
+
|
|
207
|
+
#: `data.schemaVersion` for `doctor`'s payload -- versioned independently of
|
|
208
|
+
#: `build`'s (a different `data` shape entirely: `backends[]`, not `built[]`).
|
|
209
|
+
DOCTOR_DATA_SCHEMA_VERSION = "1"
|
|
210
|
+
|
|
211
|
+
#: `data.schemaVersion` for `check`'s payload -- versioned independently of
|
|
212
|
+
#: the other two for the same reason (`models[].backends[]`, a third shape).
|
|
213
|
+
CHECK_DATA_SCHEMA_VERSION = "1"
|
|
214
|
+
|
|
215
|
+
#: `data.schemaVersion` for `list`'s payload -- versioned independently of
|
|
216
|
+
#: the other three (`models[].artifact`, a fourth shape).
|
|
217
|
+
LIST_DATA_SCHEMA_VERSION = "1"
|
|
218
|
+
|
|
219
|
+
#: `SUBCOMMANDS` names every subcommand this command accepts, in the order the
|
|
220
|
+
#: unknown-subcommand refusal lists them.
|
|
221
|
+
SUBCOMMANDS = ("build", "doctor", "check", "list", "zoo", "add", "prep", "run", "ab")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class ModelError(Exception):
|
|
225
|
+
"""A refusal whose issue code and exit code are already decided."""
|
|
226
|
+
|
|
227
|
+
def __init__(self, code: str, message: str, exit_code: ExitCode) -> None:
|
|
228
|
+
super().__init__(message)
|
|
229
|
+
self.code = code
|
|
230
|
+
self.message = message
|
|
231
|
+
self.exit_code = exit_code
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
#: Compile-opt keys that name a filesystem path (resolved relative to
|
|
235
|
+
#: board.yaml). Not every value in a models[].compile.<backend> block is a
|
|
236
|
+
#: path -- e.g. drpai's input_shape ("1,3,224,224"), input_name ("images") and
|
|
237
|
+
#: product ("V2N") are opaque strings that must reach the adapter unchanged
|
|
238
|
+
#: (alp-sdk#1271: resolving them as paths corrupted a genuine shape string into
|
|
239
|
+
#: a filesystem path, which then made the adapter's own shape check misfire).
|
|
240
|
+
_PATH_OPT_KEYS = {"config", "calibration", "images", "spec"}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _resolve_compile(block: dict | None, base: Path) -> dict | None:
|
|
244
|
+
"""Port of `model.py::_resolve_compile`: resolve known path-valued keys
|
|
245
|
+
in each per-backend compile block to an absolute path relative to the
|
|
246
|
+
`board.yaml` dir; every other value (shape strings, node names, product
|
|
247
|
+
ids, ...) passes through unchanged (alp-sdk#1271)."""
|
|
248
|
+
if not block:
|
|
249
|
+
return None
|
|
250
|
+
return {
|
|
251
|
+
backend: {
|
|
252
|
+
k: (str((base / v).resolve()) if k in _PATH_OPT_KEYS and isinstance(v, str) else v)
|
|
253
|
+
for k, v in (opts or {}).items()
|
|
254
|
+
}
|
|
255
|
+
for backend, opts in block.items()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _load_board(path: Path) -> dict[str, Any]:
|
|
260
|
+
"""`board.yaml` as a dict, or a `ModelError` for every way that can fail --
|
|
261
|
+
missing file, bad encoding, not YAML, not a mapping. `yaml.safe_load`,
|
|
262
|
+
matching the oracle's own parse exactly (unlike `system_manifest`'s
|
|
263
|
+
core-schema loader, there is no serde_yaml parity requirement here)."""
|
|
264
|
+
try:
|
|
265
|
+
text = path.read_text(encoding="utf-8")
|
|
266
|
+
except OSError as err:
|
|
267
|
+
raise ModelError(
|
|
268
|
+
"model.board-yaml-missing",
|
|
269
|
+
f"board.yaml not found at {path}: {err}",
|
|
270
|
+
ExitCode.VALIDATION_FAILURE,
|
|
271
|
+
) from err
|
|
272
|
+
except UnicodeDecodeError as err:
|
|
273
|
+
# tan-cli#396: `UnicodeDecodeError` is a `ValueError`, NOT an
|
|
274
|
+
# `OSError`, so `except OSError` alone could never catch it -- one
|
|
275
|
+
# undecodable byte in the customer's own board.yaml escaped this
|
|
276
|
+
# whole command as a traceback: measured `exitCode: 5` /
|
|
277
|
+
# `model.internal-failure` / "model build failed unexpectedly:
|
|
278
|
+
# UnicodeDecodeError: ...", which tells a script "tan broke" rather
|
|
279
|
+
# than "your board.yaml has a bad byte". The file opened and read
|
|
280
|
+
# fine, so `board-yaml-missing` would send the customer looking for
|
|
281
|
+
# the wrong problem -- this is the same unusable-input class the
|
|
282
|
+
# YAML-syntax and not-a-mapping arms below already land on.
|
|
283
|
+
#
|
|
284
|
+
# `model` has no Rust oracle to defer to here (unlike `kconfig`,
|
|
285
|
+
# which folds this same byte-for-byte case into `board-yaml-missing`
|
|
286
|
+
# to mirror `read_to_string`'s single `io::Error` arm) -- a merge
|
|
287
|
+
# briefly folded this arm into the OSError one above on that other
|
|
288
|
+
# command's precedent (tan-cli#415), which made this whole `except`
|
|
289
|
+
# unreachable dead code and silently reverted the tan-cli#396 fix.
|
|
290
|
+
# Restored as its own arm: this file's own established
|
|
291
|
+
# classification, not an oracle's.
|
|
292
|
+
raise ModelError(
|
|
293
|
+
"model.board-yaml-invalid",
|
|
294
|
+
f"{path}: not valid UTF-8: {err}",
|
|
295
|
+
ExitCode.VALIDATION_FAILURE,
|
|
296
|
+
) from err
|
|
297
|
+
try:
|
|
298
|
+
import yaml # noqa: PLC0415 (declared dependency, guarded anyway)
|
|
299
|
+
except ImportError as err:
|
|
300
|
+
raise ModelError(
|
|
301
|
+
"model.internal-failure",
|
|
302
|
+
f"no YAML parser available ({err}); install PyYAML (`pip install pyyaml`).",
|
|
303
|
+
ExitCode.INTERNAL_FAILURE,
|
|
304
|
+
) from err
|
|
305
|
+
try:
|
|
306
|
+
doc = yaml.safe_load(text)
|
|
307
|
+
except Exception as err: # noqa: BLE001 -- any PyYAML failure is bad input
|
|
308
|
+
raise ModelError(
|
|
309
|
+
"model.board-yaml-invalid", f"{path}: {err}", ExitCode.VALIDATION_FAILURE
|
|
310
|
+
) from err
|
|
311
|
+
if not isinstance(doc, dict):
|
|
312
|
+
raise ModelError(
|
|
313
|
+
"model.board-yaml-invalid",
|
|
314
|
+
f"{path}: expected a YAML mapping at the top level.",
|
|
315
|
+
ExitCode.VALIDATION_FAILURE,
|
|
316
|
+
)
|
|
317
|
+
return doc
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _require_sku(board_doc: dict, board_path: Path) -> str:
|
|
321
|
+
"""`board.yaml`'s `som.sku` -- shared by `build` and `check`, both of
|
|
322
|
+
which need a real SKU before they can resolve anything metadata-shaped
|
|
323
|
+
(a compile target, or a NPU support table)."""
|
|
324
|
+
som = board_doc.get("som")
|
|
325
|
+
sku = som.get("sku") if isinstance(som, dict) else None
|
|
326
|
+
if not isinstance(sku, str) or not sku:
|
|
327
|
+
raise ModelError(
|
|
328
|
+
"model.board-yaml-invalid",
|
|
329
|
+
f"{board_path}: som.sku is missing.",
|
|
330
|
+
ExitCode.VALIDATION_FAILURE,
|
|
331
|
+
)
|
|
332
|
+
return sku
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _require_models_list(board_doc: dict, board_path: Path) -> list:
|
|
336
|
+
"""`board.yaml`'s `models:` -- `[]` when absent (a board that declares no
|
|
337
|
+
models is valid, not an error), a `ModelError` when present but not a
|
|
338
|
+
list."""
|
|
339
|
+
models = board_doc.get("models") or []
|
|
340
|
+
if not isinstance(models, list):
|
|
341
|
+
raise ModelError(
|
|
342
|
+
"model.board-yaml-invalid",
|
|
343
|
+
f"{board_path}: `models:` must be a list.",
|
|
344
|
+
ExitCode.VALIDATION_FAILURE,
|
|
345
|
+
)
|
|
346
|
+
return models
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _require_model_entry(m: Any, board_path: Path) -> None:
|
|
350
|
+
"""Every `models:` entry needs `name` and `source` -- `build`'s own
|
|
351
|
+
per-model `compile:` block is optional and stays that command's own
|
|
352
|
+
concern; this is the shape floor both `build` and `check` share."""
|
|
353
|
+
if not isinstance(m, dict) or "name" not in m or "source" not in m:
|
|
354
|
+
raise ModelError(
|
|
355
|
+
"model.board-yaml-invalid",
|
|
356
|
+
f"{board_path}: every `models:` entry needs `name` and `source`.",
|
|
357
|
+
ExitCode.VALIDATION_FAILURE,
|
|
358
|
+
)
|
|
359
|
+
# tan-cli#1486: type-check at the shape floor so a malformed entry is the
|
|
360
|
+
# user's board.yaml's fault (exit 2), not `model.internal-failure`, and a
|
|
361
|
+
# traversal name never reaches `list`'s `<out>/<name>.alpmodel` stat.
|
|
362
|
+
name, source, compile_block = m["name"], m["source"], m.get("compile")
|
|
363
|
+
if not isinstance(name, str) or not _NAME_RE.fullmatch(name):
|
|
364
|
+
raise ModelError(
|
|
365
|
+
"model.board-yaml-invalid",
|
|
366
|
+
f"{board_path}: `models:` entry name {name!r} must be a string matching "
|
|
367
|
+
f"{_NAME_RE.pattern!r}.",
|
|
368
|
+
ExitCode.VALIDATION_FAILURE,
|
|
369
|
+
)
|
|
370
|
+
if not isinstance(source, str) or not source:
|
|
371
|
+
raise ModelError(
|
|
372
|
+
"model.board-yaml-invalid",
|
|
373
|
+
f"{board_path}: `models:` entry '{name}': `source` must be a non-empty path string.",
|
|
374
|
+
ExitCode.VALIDATION_FAILURE,
|
|
375
|
+
)
|
|
376
|
+
if compile_block is not None and (
|
|
377
|
+
not isinstance(compile_block, dict)
|
|
378
|
+
or any(o is not None and not isinstance(o, dict) for o in compile_block.values())
|
|
379
|
+
):
|
|
380
|
+
raise ModelError(
|
|
381
|
+
"model.board-yaml-invalid",
|
|
382
|
+
f"{board_path}: `models:` entry '{name}': `compile:` must be a mapping of "
|
|
383
|
+
"backend -> options mapping.",
|
|
384
|
+
ExitCode.VALIDATION_FAILURE,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _require_metadata_sdk_root(sdk_root: str | None, workspace_root: str, no_models_msg: str) -> Path:
|
|
389
|
+
"""`resolve_metadata_sdk_root`, or a coded `model.sdk-root-unresolved`
|
|
390
|
+
refusal -- shared by `build` and `check`, whose only difference is the
|
|
391
|
+
trailing "no models were {built,checked}" clause (tan-cli#497 defect 7:
|
|
392
|
+
a REJECTED `--sdk-root` names the value it rejected)."""
|
|
393
|
+
resolved_sdk = resolve_metadata_sdk_root(sdk_root, workspace_root)
|
|
394
|
+
if resolved_sdk is None:
|
|
395
|
+
raise ModelError(
|
|
396
|
+
"model.sdk-root-unresolved",
|
|
397
|
+
rejected_sdk_root_message(sdk_root, no_models_msg)
|
|
398
|
+
if sdk_root
|
|
399
|
+
# `tan sdk switch` refuses in this build (tan-cli#305) -- kept the
|
|
400
|
+
# two mechanisms that actually work here (`--sdk-root`, placing
|
|
401
|
+
# the project near a checkout) and swapped the third for
|
|
402
|
+
# NO_SDK_NEXT_STEPS's honest "how to get one at all".
|
|
403
|
+
else with_sdk_search(
|
|
404
|
+
"alp-sdk root is unresolved. Use --sdk-root, place the project near an "
|
|
405
|
+
f"alp-sdk checkout, or {NO_SDK_NEXT_STEPS}.",
|
|
406
|
+
workspace_root,
|
|
407
|
+
),
|
|
408
|
+
ExitCode.VALIDATION_FAILURE,
|
|
409
|
+
)
|
|
410
|
+
return resolved_sdk
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _resolve_metadata_dir(metadata_root: str | None, resolved_sdk: Path, workspace_root: Path) -> Path:
|
|
414
|
+
"""`<sdk-root>/metadata` unless `--metadata-root` overrides it -- shared
|
|
415
|
+
by `build` and `check`, both of which read `metadata/**` for the same
|
|
416
|
+
resolved SKU."""
|
|
417
|
+
metadata_dir = Path(metadata_root) if metadata_root else resolved_sdk / "metadata"
|
|
418
|
+
if metadata_root and not metadata_dir.is_absolute():
|
|
419
|
+
metadata_dir = workspace_root / metadata_dir
|
|
420
|
+
return metadata_dir
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _shipped_caveat_issues(name: str, out_path: Path) -> list[Issue]:
|
|
424
|
+
"""One `model.target-caveat` WARNING per caveated target in the package
|
|
425
|
+
just written -- read back OUT OF THE FILE, not out of the in-memory build.
|
|
426
|
+
|
|
427
|
+
`tan model check --exact` already surfaced the compiler's own caveats, but
|
|
428
|
+
`check` ships nothing; `build` is the path that puts bytes on a board, and
|
|
429
|
+
it dropped them. A vela blob compiled against vela's BUILT-IN default
|
|
430
|
+
profile carries an `arena`/`sram_kib` pair describing THAT memory model,
|
|
431
|
+
and those are the very figures alp-sdk's on-device selector gates on
|
|
432
|
+
(`src/backends/inference/alp_model_select.c`), so the operator who runs
|
|
433
|
+
`tan model build` has to be told -- once per target, verbatim, at the
|
|
434
|
+
moment the package is produced.
|
|
435
|
+
|
|
436
|
+
Reported from `package.read_manifest_file` so the line describes the
|
|
437
|
+
ARTIFACT: a caveat that never reached the file cannot be narrated here.
|
|
438
|
+
A readback that fails is itself a warning (`model.caveat-readback-
|
|
439
|
+
failed`), never an error -- the package IS written, and downgrading a
|
|
440
|
+
successful build to a failure over a diagnostic read would be the wrong
|
|
441
|
+
trade. It is not swallowed either: silence would be indistinguishable
|
|
442
|
+
from "no caveats"."""
|
|
443
|
+
try:
|
|
444
|
+
mft = read_manifest_file(out_path)
|
|
445
|
+
except Exception as err: # noqa: BLE001 -- a diagnostic readback, not the build
|
|
446
|
+
return [Issue("model.caveat-readback-failed", "warning",
|
|
447
|
+
f"model '{name}': wrote {out_path} but could not read its manifest "
|
|
448
|
+
f"back to report compiler caveats: {type(err).__name__}: {err}")]
|
|
449
|
+
out: list[Issue] = []
|
|
450
|
+
for target in mft.targets:
|
|
451
|
+
label = f"{target.backend} {target.accel_config}".strip()
|
|
452
|
+
for caveat in target.caveats:
|
|
453
|
+
out.append(Issue(
|
|
454
|
+
"model.target-caveat", "warning",
|
|
455
|
+
f"model '{name}': the {label} target in {out_path.name} ships with a "
|
|
456
|
+
f"compiler caveat -- {caveat}"))
|
|
457
|
+
return out
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _run_build(
|
|
461
|
+
*,
|
|
462
|
+
context: ProjectContext,
|
|
463
|
+
out: str,
|
|
464
|
+
metadata_root: str | None,
|
|
465
|
+
sdk_root: str | None,
|
|
466
|
+
) -> tuple[Project, SdkInfo | None, dict, list[Issue], ExitCode]:
|
|
467
|
+
"""`--board` plays the role `--board-yaml` does everywhere else, so the
|
|
468
|
+
SAME project-context resolution applies (I-31); the envelope's `project`
|
|
469
|
+
and `sdk` fields come from that ONE resolution, matching `size`/`image`
|
|
470
|
+
(`build_output.resolve_project_context`'s own doc: the envelope's `sdk`
|
|
471
|
+
block must be what THAT resolution produced, never a second lookup).
|
|
472
|
+
|
|
473
|
+
tan-cli#497 defect 3: the `resolve_project_context` call used to live at
|
|
474
|
+
the top of this function, which meant its `broken_project_pin` /
|
|
475
|
+
`foreign_global_default_for` were unreachable from `model`'s own
|
|
476
|
+
`except ModelError` handler -- so the pin warning was dropped on the
|
|
477
|
+
refusal paths as well as the happy one. It is now resolved by the caller
|
|
478
|
+
and handed in, so the ONE resolution feeds every exit."""
|
|
479
|
+
workspace_root = Path(context.workspace_root)
|
|
480
|
+
board_path = Path(context.board_yaml)
|
|
481
|
+
reported_project = context.project()
|
|
482
|
+
sdk_info = context.sdk
|
|
483
|
+
|
|
484
|
+
# The metadata-reading resolution is DELIBERATELY separate and wider
|
|
485
|
+
# (`build_output.resolve_metadata_sdk_root`'s own doc): a child/sibling
|
|
486
|
+
# checkout the project-context tier chain does not consider can still
|
|
487
|
+
# supply `metadata/`, in which case `sdk_info` stays absent while the
|
|
488
|
+
# build still runs against it -- same divergence `tan size`'s budget
|
|
489
|
+
# resolution already allows.
|
|
490
|
+
resolved_sdk = _require_metadata_sdk_root(sdk_root, context.workspace_root, "No models were built.")
|
|
491
|
+
|
|
492
|
+
board_doc = _load_board(board_path)
|
|
493
|
+
sku = _require_sku(board_doc, board_path)
|
|
494
|
+
models = _require_models_list(board_doc, board_path)
|
|
495
|
+
|
|
496
|
+
data: dict[str, Any] = {"schemaVersion": DATA_SCHEMA_VERSION, "sku": sku, "built": []}
|
|
497
|
+
if not models:
|
|
498
|
+
return reported_project, sdk_info, data, [], ExitCode.SUCCESS
|
|
499
|
+
|
|
500
|
+
base = board_path.parent
|
|
501
|
+
out_dir = Path(out)
|
|
502
|
+
if not out_dir.is_absolute():
|
|
503
|
+
out_dir = workspace_root / out_dir
|
|
504
|
+
metadata_dir = _resolve_metadata_dir(metadata_root, resolved_sdk, workspace_root)
|
|
505
|
+
|
|
506
|
+
# Validate + prepare every model BEFORE building any of them -- an invalid
|
|
507
|
+
# `models:` entry refuses the whole run with no partial build, matching
|
|
508
|
+
# the pre-ADR-0028 shape where this same loop only assembled the driver
|
|
509
|
+
# payload and never itself built anything.
|
|
510
|
+
prepared: list[tuple[str, Path, dict | None]] = []
|
|
511
|
+
for m in models:
|
|
512
|
+
_require_model_entry(m, board_path)
|
|
513
|
+
source = (base / m["source"]).resolve()
|
|
514
|
+
prepared.append((m["name"], source, _resolve_compile(m.get("compile"), base)))
|
|
515
|
+
|
|
516
|
+
# Deliberate divergence 1 from the oracle (see module doc): a per-model
|
|
517
|
+
# `build_model()` failure is caught here and reported as a coded
|
|
518
|
+
# `model.build-failed` issue, and the loop continues to the next model
|
|
519
|
+
# rather than aborting the whole batch.
|
|
520
|
+
issues: list[Issue] = []
|
|
521
|
+
built: list[str] = []
|
|
522
|
+
for name, source, compile_opts in prepared:
|
|
523
|
+
try:
|
|
524
|
+
out_path = build_model(
|
|
525
|
+
sku=sku,
|
|
526
|
+
name=name,
|
|
527
|
+
source=source,
|
|
528
|
+
out_dir=out_dir,
|
|
529
|
+
metadata_root=metadata_dir,
|
|
530
|
+
compile_opts=compile_opts,
|
|
531
|
+
board_doc=board_doc,
|
|
532
|
+
)
|
|
533
|
+
except SramNoFitRefused as err:
|
|
534
|
+
# tan-cli#1288: a certain arena/SRAM0 no-fit under `Sram_Only` --
|
|
535
|
+
# ahead of the generic `except Exception` below so it reports its
|
|
536
|
+
# OWN code/exit, not `model.build-failed`/`WRITE_FAILURE`.
|
|
537
|
+
issues.append(Issue("model.sram-no-fit", "error", f"model '{name}': {err}"))
|
|
538
|
+
except Exception as err: # noqa: BLE001 -- a per-model failure is a coded issue, not a traceback
|
|
539
|
+
issues.append(
|
|
540
|
+
Issue(
|
|
541
|
+
"model.build-failed",
|
|
542
|
+
"error",
|
|
543
|
+
f"model '{name}': {type(err).__name__}: {err}",
|
|
544
|
+
)
|
|
545
|
+
)
|
|
546
|
+
else:
|
|
547
|
+
built.append(str(out_path))
|
|
548
|
+
issues.extend(_shipped_caveat_issues(name, out_path))
|
|
549
|
+
data["built"] = built
|
|
550
|
+
# ERRORS decide the exit code, not `issues` being non-empty. `_shipped_
|
|
551
|
+
# caveat_issues` puts WARNINGS in this list, and a warning about a package
|
|
552
|
+
# that was written must not report it as a write failure -- the same
|
|
553
|
+
# separation `finish()`'s renderer already makes between `warnings` and
|
|
554
|
+
# `errors`, and the same one the caller's `sdk_issues` rely on.
|
|
555
|
+
#
|
|
556
|
+
# `model.sram-no-fit` gets its OWN exit code (tan-cli#1288's binding
|
|
557
|
+
# decision) -- checked first so it is never masked by the generic
|
|
558
|
+
# WRITE_FAILURE a `model.build-failed` alongside it would otherwise pick.
|
|
559
|
+
if any(i.code == "model.sram-no-fit" for i in issues):
|
|
560
|
+
exit_code = ExitCode.VALIDATION_FAILURE
|
|
561
|
+
elif any(i.severity == "error" for i in issues):
|
|
562
|
+
exit_code = ExitCode.WRITE_FAILURE
|
|
563
|
+
else:
|
|
564
|
+
exit_code = ExitCode.SUCCESS
|
|
565
|
+
return reported_project, sdk_info, data, issues, exit_code
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _declared_hw_rev(board_doc: dict, board_path: Path) -> str | None:
|
|
569
|
+
"""`board.yaml`'s `som.hw_rev`, or `None` when it declares none.
|
|
570
|
+
|
|
571
|
+
OPTIONAL by `board.schema.json`, which also states the fallback: "the SDK
|
|
572
|
+
falls back to the SKU preset's `default_hw_rev` when omitted". That
|
|
573
|
+
fallback is applied downstream (`tan.model.perf_apply._resolve_hw_rev`),
|
|
574
|
+
where the metadata root is in hand; this only reports what the customer
|
|
575
|
+
wrote. Unlike `som.sku` a missing value is not a refusal -- `check` worked
|
|
576
|
+
without it before a bench measurement existed to pin to a revision, and
|
|
577
|
+
must keep working.
|
|
578
|
+
|
|
579
|
+
A PRESENT-BUT-UNUSABLE value fails CLOSED instead, rather than silently
|
|
580
|
+
falling through to the same `None` a genuinely absent key returns
|
|
581
|
+
(tan-cli#791 review MINOR 5). `board.yaml` is YAML: an unquoted
|
|
582
|
+
`hw_rev: 2` parses to the int `2`, not the string `"r2"` no perf point is
|
|
583
|
+
ever published under. Measured: `{"som": {"hw_rev": 2}} -> None` here
|
|
584
|
+
used to silently defer to the SKU preset's own `default_hw_rev` --
|
|
585
|
+
serving a customer who wrote a real hw_rev a DIFFERENT module revision's
|
|
586
|
+
bench measurement, exactly the "describes a different machine" failure
|
|
587
|
+
`hw_rev` exists to refuse in the first place (alp-sdk `f724d3e4`).
|
|
588
|
+
`_run_check` does not otherwise schema-validate `board.yaml`, so this is
|
|
589
|
+
the one place this particular shape of bad input is ever caught -- raised
|
|
590
|
+
as a `ModelError` (a board-level fact, refusing the WHOLE run, same
|
|
591
|
+
scoping `_require_sku`/`_require_check_backends` already use) rather than
|
|
592
|
+
returned, since a caller that got `None` back has no way to tell "the
|
|
593
|
+
customer said nothing" apart from "the customer said something tan could
|
|
594
|
+
not use"."""
|
|
595
|
+
som = board_doc.get("som")
|
|
596
|
+
if not isinstance(som, dict) or "hw_rev" not in som:
|
|
597
|
+
return None
|
|
598
|
+
hw_rev = som["hw_rev"]
|
|
599
|
+
if isinstance(hw_rev, str) and hw_rev:
|
|
600
|
+
return hw_rev
|
|
601
|
+
raise ModelError(
|
|
602
|
+
"model.board-yaml-invalid",
|
|
603
|
+
f"{board_path}: som.hw_rev must be a non-empty string (e.g. \"r2\"); got {hw_rev!r}.",
|
|
604
|
+
ExitCode.VALIDATION_FAILURE,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _check_one_model(name: str, source: Path, backends: list[str], sku: str,
|
|
609
|
+
metadata_dir: Path, exact: bool,
|
|
610
|
+
hw_rev: str | None = None,
|
|
611
|
+
board_doc: dict | None = None) -> dict | Issue:
|
|
612
|
+
"""One declared model's `check` result: the serialised `{name, source,
|
|
613
|
+
backends}` block on success, or a coded `model.check-failed` Issue on any
|
|
614
|
+
failure (an unreadable/unparseable source, most commonly) -- never a
|
|
615
|
+
traceback, matching `build`'s own per-model `model.build-failed` shape so
|
|
616
|
+
one bad model in a multi-model board.yaml does not abort the batch."""
|
|
617
|
+
try:
|
|
618
|
+
reports = check_model_backends(backends=backends, sku=sku, source=source,
|
|
619
|
+
metadata_root=metadata_dir, exact=exact,
|
|
620
|
+
hw_rev=hw_rev, board_doc=board_doc)
|
|
621
|
+
except Exception as err: # noqa: BLE001 -- a per-model failure is a coded issue, not a traceback
|
|
622
|
+
return Issue("model.check-failed", "error", f"model '{name}': {type(err).__name__}: {err}")
|
|
623
|
+
return {"name": name, "source": str(source),
|
|
624
|
+
"backends": [backend_report_as_dict(r) for r in reports]}
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _sram_no_fit_issues(name: str, backends: list[dict]) -> list[Issue]:
|
|
628
|
+
"""One `model.sram-no-fit` Issue per backend whose `sramFit` block (tan-
|
|
629
|
+
cli#1288) reports a certain NO-FIT on either axis -- `check --exact`'s own
|
|
630
|
+
half of the binding decision: the model's serialised report still carries
|
|
631
|
+
the `sramFit` numbers (nothing here hides the report), but the run also
|
|
632
|
+
gets a coded, `error`-severity issue so `--exact` over a `Sram_Only` SKU
|
|
633
|
+
cannot exit 0 on a placement that cannot execute."""
|
|
634
|
+
issues: list[Issue] = []
|
|
635
|
+
for backend in backends:
|
|
636
|
+
fit = backend.get("sramFit")
|
|
637
|
+
if not fit:
|
|
638
|
+
continue
|
|
639
|
+
bad = [axis for axis in ("arena", "sram0") if fit[axis]["verdict"] == "no-fit"]
|
|
640
|
+
if not bad:
|
|
641
|
+
continue
|
|
642
|
+
detail = "; ".join(
|
|
643
|
+
f"{axis} needs {fit[axis]['neededKib']} KiB, only "
|
|
644
|
+
f"{fit[axis]['limitKib']} KiB available"
|
|
645
|
+
for axis in bad
|
|
646
|
+
)
|
|
647
|
+
variant = backend.get("variant")
|
|
648
|
+
label = f"{backend['backend']}" + (f" ({variant})" if variant else "")
|
|
649
|
+
issues.append(Issue("model.sram-no-fit", "error",
|
|
650
|
+
f"model '{name}' {label}: {detail}"))
|
|
651
|
+
return issues
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _require_check_backends(sku: str, metadata_dir: Path, board_path: Path) -> list[str]:
|
|
655
|
+
"""`resolve_check_backends`, or a coded `model.check-sku-unresolved`
|
|
656
|
+
refusal -- a board-level fact about `som.sku`, reported once for the
|
|
657
|
+
whole run rather than once per declared model."""
|
|
658
|
+
try:
|
|
659
|
+
return resolve_check_backends(sku, metadata_root=metadata_dir)
|
|
660
|
+
except (FileNotFoundError, ValueError) as err:
|
|
661
|
+
raise ModelError(
|
|
662
|
+
"model.check-sku-unresolved",
|
|
663
|
+
f"{board_path}: could not resolve NPU backends for som.sku {sku!r}: {err}",
|
|
664
|
+
ExitCode.VALIDATION_FAILURE,
|
|
665
|
+
) from err
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _run_check(
|
|
669
|
+
*,
|
|
670
|
+
context: ProjectContext,
|
|
671
|
+
metadata_root: str | None,
|
|
672
|
+
sdk_root: str | None,
|
|
673
|
+
exact: bool,
|
|
674
|
+
) -> tuple[Project, SdkInfo | None, dict, list[Issue], ExitCode]:
|
|
675
|
+
"""Static NPU-eligibility screen for every model `board.yaml` declares --
|
|
676
|
+
same project-context/SDK-root resolution shape as `_run_build` (see its
|
|
677
|
+
own docstring for why that resolution happens in the caller). `ok` stays
|
|
678
|
+
`True`/exit 0 for a run that completed, whatever the verdicts read --
|
|
679
|
+
reporting `undetermined`/`cpu-only` IS the feature; only a run that could
|
|
680
|
+
not complete (an unresolved SKU, a per-model read failure) is non-zero."""
|
|
681
|
+
workspace_root = Path(context.workspace_root)
|
|
682
|
+
board_path = Path(context.board_yaml)
|
|
683
|
+
reported_project = context.project()
|
|
684
|
+
sdk_info = context.sdk
|
|
685
|
+
|
|
686
|
+
resolved_sdk = _require_metadata_sdk_root(sdk_root, context.workspace_root, "No models were checked.")
|
|
687
|
+
|
|
688
|
+
board_doc = _load_board(board_path)
|
|
689
|
+
sku = _require_sku(board_doc, board_path)
|
|
690
|
+
models = _require_models_list(board_doc, board_path)
|
|
691
|
+
|
|
692
|
+
data: dict[str, Any] = {
|
|
693
|
+
"schemaVersion": CHECK_DATA_SCHEMA_VERSION, "sku": sku, "exact": exact, "models": [],
|
|
694
|
+
}
|
|
695
|
+
if not models:
|
|
696
|
+
return reported_project, sdk_info, data, [], ExitCode.SUCCESS
|
|
697
|
+
|
|
698
|
+
base = board_path.parent
|
|
699
|
+
metadata_dir = _resolve_metadata_dir(metadata_root, resolved_sdk, workspace_root)
|
|
700
|
+
backends = _require_check_backends(sku, metadata_dir, board_path)
|
|
701
|
+
# Board-level, like `sku`/`backends` above: resolved ONCE, ahead of the
|
|
702
|
+
# per-model loop, so an unusable `som.hw_rev` (`ModelError`, see
|
|
703
|
+
# `_declared_hw_rev`) refuses the whole run instead of repeating the same
|
|
704
|
+
# parse -- and the same failure -- once per declared model.
|
|
705
|
+
declared_hw_rev = _declared_hw_rev(board_doc, board_path)
|
|
706
|
+
|
|
707
|
+
issues: list[Issue] = []
|
|
708
|
+
model_reports: list[dict] = []
|
|
709
|
+
for m in models:
|
|
710
|
+
_require_model_entry(m, board_path)
|
|
711
|
+
source = (base / m["source"]).resolve()
|
|
712
|
+
result = _check_one_model(m["name"], source, backends, sku, metadata_dir, exact,
|
|
713
|
+
declared_hw_rev, board_doc)
|
|
714
|
+
if isinstance(result, Issue):
|
|
715
|
+
issues.append(result)
|
|
716
|
+
else:
|
|
717
|
+
model_reports.append(result)
|
|
718
|
+
issues.extend(_sram_no_fit_issues(m["name"], result["backends"]))
|
|
719
|
+
data["models"] = model_reports
|
|
720
|
+
# `model.sram-no-fit` gets its OWN exit code (tan-cli#1288's binding
|
|
721
|
+
# decision), same precedence `_run_build` gives it -- checked first so it
|
|
722
|
+
# is never masked by the generic RUNTIME_FAILURE a `model.check-failed`
|
|
723
|
+
# alongside it would otherwise pick.
|
|
724
|
+
if any(i.code == "model.sram-no-fit" for i in issues):
|
|
725
|
+
exit_code = ExitCode.VALIDATION_FAILURE
|
|
726
|
+
elif issues:
|
|
727
|
+
exit_code = ExitCode.RUNTIME_FAILURE
|
|
728
|
+
else:
|
|
729
|
+
exit_code = ExitCode.SUCCESS
|
|
730
|
+
return reported_project, sdk_info, data, issues, exit_code
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _run_list(
|
|
734
|
+
*,
|
|
735
|
+
context: ProjectContext,
|
|
736
|
+
out: str,
|
|
737
|
+
) -> tuple[Project, SdkInfo | None, dict, list[Issue], ExitCode]:
|
|
738
|
+
"""Every declared model next to what `--out` already holds for it (module
|
|
739
|
+
doc): no `--sdk-root`/`metadata_root` handling at all, unlike `_run_build`/
|
|
740
|
+
`_run_check` above -- `list` reads only `board.yaml` and `out`'s own
|
|
741
|
+
directory, so there is nothing here to resolve a checkout for. Never
|
|
742
|
+
refuses over a missing/invalid `som.sku` either (`declared_sku` degrades
|
|
743
|
+
to `None`): the SoM is not needed to say what is declared and what is
|
|
744
|
+
already on disk."""
|
|
745
|
+
workspace_root = Path(context.workspace_root)
|
|
746
|
+
board_path = Path(context.board_yaml)
|
|
747
|
+
reported_project = context.project()
|
|
748
|
+
sdk_info = context.sdk
|
|
749
|
+
|
|
750
|
+
board_doc = _load_board(board_path)
|
|
751
|
+
sku = declared_sku(board_doc)
|
|
752
|
+
models = _require_models_list(board_doc, board_path)
|
|
753
|
+
|
|
754
|
+
data: dict[str, Any] = {"schemaVersion": LIST_DATA_SCHEMA_VERSION, "sku": sku, "models": []}
|
|
755
|
+
if not models:
|
|
756
|
+
return reported_project, sdk_info, data, [], ExitCode.SUCCESS
|
|
757
|
+
|
|
758
|
+
base = board_path.parent
|
|
759
|
+
out_dir = Path(out)
|
|
760
|
+
if not out_dir.is_absolute():
|
|
761
|
+
out_dir = workspace_root / out_dir
|
|
762
|
+
|
|
763
|
+
entries = []
|
|
764
|
+
issues: list[Issue] = []
|
|
765
|
+
for m in models:
|
|
766
|
+
_require_model_entry(m, board_path)
|
|
767
|
+
source = (base / m["source"]).resolve()
|
|
768
|
+
entry, issue = list_entry(m["name"], source, out_dir)
|
|
769
|
+
entries.append(entry)
|
|
770
|
+
if issue is not None:
|
|
771
|
+
issues.append(issue)
|
|
772
|
+
data["models"] = entries
|
|
773
|
+
return reported_project, sdk_info, data, issues, ExitCode.SUCCESS
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _backend_version(backend: str, *, available: bool) -> str | None:
|
|
777
|
+
"""Best-effort compiler-version string for an AVAILABLE backend --
|
|
778
|
+
READ-ONLY, never a spawn (module doc). `None` whenever no non-spawning
|
|
779
|
+
probe exists, or the one that does answers a DEGRADED sentinel rather
|
|
780
|
+
than a real version:
|
|
781
|
+
|
|
782
|
+
* `ethos_u` -- `_vela_version()` reads `importlib.metadata`, no
|
|
783
|
+
subprocess. It returns the bare literal `"vela"` when the
|
|
784
|
+
`ethos-u-vela` distribution's metadata is absent
|
|
785
|
+
(`except PackageNotFoundError`) -- that is a degraded answer, not a
|
|
786
|
+
version, so it is reported here as `None` rather than surfaced as if
|
|
787
|
+
it were one.
|
|
788
|
+
* `drpai` -- `_compiler_version(tvm_home)` reads a version file inside
|
|
789
|
+
the toolchain checkout (`tan/model/adapters/drpai.py`), also no
|
|
790
|
+
subprocess. It returns the bare literal `"drp-ai_tvm"` when none of
|
|
791
|
+
`setup/version` / `version` / `VERSION` is found under `tvm_home` --
|
|
792
|
+
that is the identical degraded-sentinel shape `ethos_u` guards above,
|
|
793
|
+
so it gets the same guard: reported here as `None` rather than
|
|
794
|
+
surfaced as if it were a real version.
|
|
795
|
+
* `deepx_dxm1` -- `_dxcom_version()` is the only version probe DeepxAdapter
|
|
796
|
+
has, and it SPAWNS (`subprocess.run(["dxcom", "-v"], ...)`,
|
|
797
|
+
`tan/model/adapters/deepx.py`). `doctor` must never invoke a compiler,
|
|
798
|
+
so this backend's version is always unknown here, never guessed at.
|
|
799
|
+
* `cpu` -- no external tool at all (`BACKEND_TOOLS["cpu"] is None`), so
|
|
800
|
+
no version to report.
|
|
801
|
+
"""
|
|
802
|
+
if not available:
|
|
803
|
+
return None
|
|
804
|
+
if backend == "ethos_u":
|
|
805
|
+
version = _vela_version()
|
|
806
|
+
return None if version == "vela" else version
|
|
807
|
+
if backend == "drpai":
|
|
808
|
+
tvm_home = _drpai_tvm_home()
|
|
809
|
+
if tvm_home is None:
|
|
810
|
+
return None
|
|
811
|
+
version = _drpai_compiler_version(tvm_home)
|
|
812
|
+
return None if version == "drp-ai_tvm" else version
|
|
813
|
+
return None
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _deepx_dxm1_status() -> tuple[bool, str | None]:
|
|
817
|
+
"""`deepx_dxm1`'s doctor verdict -- gated on what `DeepxAdapter.compile()`
|
|
818
|
+
actually needs (the bare `dxcom` off PATH: `cmd = ["dxcom", "-m", ...]`,
|
|
819
|
+
`tan/model/adapters/deepx.py`), NOT `DeepxAdapter.is_available()`.
|
|
820
|
+
|
|
821
|
+
That adapter method ORs in a second arm -- `ALP_DEEPX_SDK_HOME` naming a
|
|
822
|
+
directory -- that `compile()` never reads at all, so a row gated on it
|
|
823
|
+
reported `available: true` on a host with the env var pointed at an
|
|
824
|
+
empty directory and no `dxcom` anywhere, and the very next `model build`
|
|
825
|
+
raised `FileNotFoundError: [Errno 2] No such file or directory: 'dxcom'`.
|
|
826
|
+
Deliberately NOT a change to `DeepxAdapter.is_available()` itself --
|
|
827
|
+
`tan.model.build.build_model` uses that method for a different decision
|
|
828
|
+
(whether to attempt this backend at all) that this env-var arm is
|
|
829
|
+
legitimate for; this is doctor's own, narrower "would compile() actually
|
|
830
|
+
run" question, answered without spawning anything.
|
|
831
|
+
|
|
832
|
+
Read-only: `shutil.which` + `os.environ.get` + `Path.is_dir`, the exact
|
|
833
|
+
same primitives `is_available()` itself uses, never a subprocess.
|
|
834
|
+
|
|
835
|
+
Returns `(available, reason)`. `reason` is `None` when either `dxcom` is
|
|
836
|
+
on PATH (row is available, no reason needed) or neither PATH nor the env
|
|
837
|
+
var carries any signal at all (the caller falls back to
|
|
838
|
+
`_UNAVAILABLE_REASONS["deepx_dxm1"]`'s generic reason). When the env var
|
|
839
|
+
IS set to a real directory but `dxcom` still isn't on PATH, `reason`
|
|
840
|
+
carries an explicit caveat -- the var is set, but it is not what
|
|
841
|
+
`compile()` reads -- rather than silently reporting green.
|
|
842
|
+
"""
|
|
843
|
+
if shutil.which("dxcom"):
|
|
844
|
+
return True, None
|
|
845
|
+
sdk_home = os.environ.get("ALP_DEEPX_SDK_HOME")
|
|
846
|
+
if sdk_home and Path(sdk_home).is_dir():
|
|
847
|
+
return False, (
|
|
848
|
+
f"ALP_DEEPX_SDK_HOME={sdk_home} is set, but dxcom is not on PATH; "
|
|
849
|
+
"DeepxAdapter.compile() always shells the bare `dxcom` off PATH "
|
|
850
|
+
"and never reads ALP_DEEPX_SDK_HOME, so this environment would "
|
|
851
|
+
"still fail `model build`"
|
|
852
|
+
)
|
|
853
|
+
return False, None
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _drpai_status() -> tuple[bool, str | None]:
|
|
857
|
+
"""`drpai`'s doctor verdict -- gated on what `DrpaiAdapter.compile()`
|
|
858
|
+
actually needs under `$ALP_DRPAI_TVM_HOME`: the vendor tutorial script
|
|
859
|
+
`tutorials/compile_onnx_model_quant.py` it spawns AND the `python3`
|
|
860
|
+
interpreter it shells that script with (`tan/model/adapters/drpai.py`'s
|
|
861
|
+
own `cmd = ["python3", str(script), ...]`), NOT just the bare directory
|
|
862
|
+
`DrpaiAdapter.is_available()`/`_tvm_home()` check for.
|
|
863
|
+
|
|
864
|
+
A customer who points `ALP_DRPAI_TVM_HOME` at an unpacked-but-unbuilt
|
|
865
|
+
checkout (the tutorials/ tree not yet present, or laid out differently)
|
|
866
|
+
got `available: true` from the bare directory check, then a vendor-script
|
|
867
|
+
failure on the next real build -- and a host with a BUILT tree but no
|
|
868
|
+
`python3` on PATH hit the exact same false-green class one dependency
|
|
869
|
+
further out, still reading green right up until that same `model build`
|
|
870
|
+
failed. Deliberately NOT a change to `DrpaiAdapter.is_available()`/
|
|
871
|
+
`_tvm_home()` themselves -- same reasoning as `_deepx_dxm1_status` above:
|
|
872
|
+
this is doctor's own, narrower question. `python3` is deliberately NOT
|
|
873
|
+
`BACKEND_TOOLS["drpai"]` (see that dict's own comment: naming an
|
|
874
|
+
interpreter present on essentially every host tells a customer nothing
|
|
875
|
+
actionable) -- it is checked here as a `compile()` prerequisite, not
|
|
876
|
+
reported as *the* tool this row names.
|
|
877
|
+
|
|
878
|
+
Read-only: `_tvm_home()` (env var + `Path.is_dir`) + `Path.is_file` on the
|
|
879
|
+
tutorial script path + `shutil.which("python3")`, never a subprocess.
|
|
880
|
+
|
|
881
|
+
Returns `(available, reason)`, same shape as `_deepx_dxm1_status`: `None`
|
|
882
|
+
reason falls back to `_UNAVAILABLE_REASONS["drpai"]`'s generic reason
|
|
883
|
+
(env var not set at all); an explicit caveat when the var names a real
|
|
884
|
+
directory but the tutorial script isn't under it, or when both are
|
|
885
|
+
present but `python3` is not on PATH.
|
|
886
|
+
"""
|
|
887
|
+
tvm_home = _drpai_tvm_home()
|
|
888
|
+
if tvm_home is None:
|
|
889
|
+
return False, None
|
|
890
|
+
script = tvm_home / "tutorials" / "compile_onnx_model_quant.py"
|
|
891
|
+
if not script.is_file():
|
|
892
|
+
return False, (
|
|
893
|
+
f"ALP_DRPAI_TVM_HOME={tvm_home} is set, but tutorials/"
|
|
894
|
+
"compile_onnx_model_quant.py was not found under it -- point it at "
|
|
895
|
+
"a BUILT rzv_drp-ai_tvm install, not an unpacked/incomplete tree"
|
|
896
|
+
)
|
|
897
|
+
if shutil.which("python3") is None:
|
|
898
|
+
return False, (
|
|
899
|
+
f"ALP_DRPAI_TVM_HOME={tvm_home} and tutorials/"
|
|
900
|
+
"compile_onnx_model_quant.py are both present, but python3 is "
|
|
901
|
+
"not on PATH -- DrpaiAdapter.compile() shells `python3 "
|
|
902
|
+
"<script> ...` (tan/model/adapters/drpai.py), so this "
|
|
903
|
+
"environment would still fail `model build`"
|
|
904
|
+
)
|
|
905
|
+
return True, None
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def _vela_vendor_config_status() -> tuple[bool, str | None]:
|
|
909
|
+
"""The OPTIONAL vendor vela `.ini` row's verdict -- is `ALP_VELA_CONFIG`
|
|
910
|
+
pointing at a file `VelaAdapter.compile()` could pass to `vela --config`?
|
|
911
|
+
|
|
912
|
+
Read-only and non-spawning like every other probe here: `os.environ.get` +
|
|
913
|
+
`Path.is_file`, through the adapter's own `_vendor_config_path()` so doctor
|
|
914
|
+
and the compile answer the same question from one implementation. It reads
|
|
915
|
+
no metadata: WHICH vendor file a given part wants is a per-SoC fact
|
|
916
|
+
(`npu_toolchain.vela.vendor_config_filename`), and doctor reports
|
|
917
|
+
host-toolchain facts that hold whatever project it was run from.
|
|
918
|
+
|
|
919
|
+
Returns `(available, reason)`. The reason MUST make it plain that absence
|
|
920
|
+
is not a fault: without this file vela uses Arm's own built-in system
|
|
921
|
+
config, which is exactly what tan's reported arena/SRAM figures describe --
|
|
922
|
+
the vendor file buys a vendor-tuned bandwidth/latency model, not
|
|
923
|
+
correctness. A customer without a license is complete, not broken.
|
|
924
|
+
"""
|
|
925
|
+
raw = os.environ.get(_VELA_CONFIG_ENV)
|
|
926
|
+
if not raw:
|
|
927
|
+
return False, (
|
|
928
|
+
f"OPTIONAL, not a fault: {_VELA_CONFIG_ENV} is not set, so vela uses Arm's "
|
|
929
|
+
"built-in system config -- the arena/SRAM figures tan reports describe that "
|
|
930
|
+
"model and are correct. A licensed customer may set it to their vendor vela "
|
|
931
|
+
"config .ini (e.g. Alif's ensemble_vela.ini) for the vendor-tuned profile"
|
|
932
|
+
)
|
|
933
|
+
if _vendor_config_path() is None:
|
|
934
|
+
return False, (
|
|
935
|
+
f"OPTIONAL, not a fault: {_VELA_CONFIG_ENV}={raw} does not name a readable "
|
|
936
|
+
"file, so it is ignored and vela uses Arm's built-in system config -- point "
|
|
937
|
+
"it at the vendor vela config .ini or unset it"
|
|
938
|
+
)
|
|
939
|
+
return True, None
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def _probe_backend(backend: str) -> tuple[bool, str | None]:
|
|
943
|
+
"""`(available, reason override)` for one backend row -- `deepx_dxm1` and
|
|
944
|
+
`drpai` each get a NARROWER probe than the adapter's own `is_available()`
|
|
945
|
+
(see `_deepx_dxm1_status`/`_drpai_status`'s own docs): `is_available()`
|
|
946
|
+
reports green on a signal `compile()` doesn't actually act on, so gating
|
|
947
|
+
a row on it can report available when the next `model build` would
|
|
948
|
+
immediately fail. Every other backend keeps the plain `is_available()`
|
|
949
|
+
OR across its registered adapters, with no reason override."""
|
|
950
|
+
if backend == "deepx_dxm1":
|
|
951
|
+
return _deepx_dxm1_status()
|
|
952
|
+
if backend == "drpai":
|
|
953
|
+
return _drpai_status()
|
|
954
|
+
available = any(a.is_available() for a in _ADAPTERS if a.backend == backend)
|
|
955
|
+
return available, None
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def _run_doctor(
|
|
959
|
+
*,
|
|
960
|
+
context: ProjectContext,
|
|
961
|
+
sdk_root: str | None,
|
|
962
|
+
) -> tuple[Project, SdkInfo | None, dict, list[Issue], ExitCode]:
|
|
963
|
+
"""One row per registered compiler-backend, per the module doc: READ-ONLY,
|
|
964
|
+
spawns nothing, and never fails the run -- an unavailable toolchain is
|
|
965
|
+
reported, not refused. A board.yaml is not read at all: unlike `build`,
|
|
966
|
+
`doctor` has no per-model work to resolve, only host-toolchain facts that
|
|
967
|
+
hold regardless of which (if any) project this was run from.
|
|
968
|
+
|
|
969
|
+
The SDK root is still resolved, mirroring `_run_build` (module doc), but
|
|
970
|
+
ONLY to decide whether `model.doctor-sdk-unresolved` belongs in `issues`
|
|
971
|
+
-- a WARNING, never the VALIDATION_FAILURE `_run_build` raises for the
|
|
972
|
+
same non-resolution, since nothing below actually needs it: no backend
|
|
973
|
+
row reads `metadata/**`.
|
|
974
|
+
"""
|
|
975
|
+
reported_project = context.project()
|
|
976
|
+
sdk_info = context.sdk
|
|
977
|
+
|
|
978
|
+
issues: list[Issue] = []
|
|
979
|
+
resolved_sdk = resolve_metadata_sdk_root(sdk_root, context.workspace_root)
|
|
980
|
+
if resolved_sdk is None:
|
|
981
|
+
issues.append(
|
|
982
|
+
Issue(
|
|
983
|
+
"model.doctor-sdk-unresolved",
|
|
984
|
+
"warning",
|
|
985
|
+
(
|
|
986
|
+
rejected_sdk_root_message(
|
|
987
|
+
sdk_root, "Backend availability below is unaffected."
|
|
988
|
+
)
|
|
989
|
+
if sdk_root
|
|
990
|
+
else with_sdk_search(
|
|
991
|
+
"alp-sdk root is unresolved. Use --sdk-root, place the project "
|
|
992
|
+
f"near an alp-sdk checkout, or {NO_SDK_NEXT_STEPS}. Backend "
|
|
993
|
+
"availability below is unaffected.",
|
|
994
|
+
context.workspace_root,
|
|
995
|
+
)
|
|
996
|
+
),
|
|
997
|
+
)
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
rows = []
|
|
1001
|
+
for backend in registry_backends(_ADAPTERS):
|
|
1002
|
+
available, reason = _probe_backend(backend)
|
|
1003
|
+
version = _backend_version(backend, available=available)
|
|
1004
|
+
rows.append(backend_row(backend, available=available, version=version, reason=reason))
|
|
1005
|
+
|
|
1006
|
+
# OPTIONAL prerequisites, reported separately from `backends[]` so an
|
|
1007
|
+
# absent one cannot read as a broken backend (`model_doctor.optional_row`).
|
|
1008
|
+
vendor_config_available, vendor_config_reason = _vela_vendor_config_status()
|
|
1009
|
+
optional_rows = [
|
|
1010
|
+
optional_row("ethos_u", tool=_VELA_CONFIG_ENV,
|
|
1011
|
+
available=vendor_config_available, reason=vendor_config_reason),
|
|
1012
|
+
]
|
|
1013
|
+
|
|
1014
|
+
data: dict[str, Any] = {
|
|
1015
|
+
"schemaVersion": DOCTOR_DATA_SCHEMA_VERSION,
|
|
1016
|
+
"backends": [row.as_dict() for row in rows],
|
|
1017
|
+
# Additive: `schemaVersion` stays "1". A consumer reading `backends[]`
|
|
1018
|
+
# sees the same list it always did, and one that does not know this key
|
|
1019
|
+
# ignores it -- which is the correct outcome for an enhancement nobody
|
|
1020
|
+
# is required to have.
|
|
1021
|
+
"optional": [row.as_dict() for row in optional_rows],
|
|
1022
|
+
}
|
|
1023
|
+
return reported_project, sdk_info, data, issues, ExitCode.SUCCESS
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def _refuse_stray_arguments(
|
|
1027
|
+
subcommand: str,
|
|
1028
|
+
model_id: str | None,
|
|
1029
|
+
sku: str | None,
|
|
1030
|
+
device_flags: tuple[bool, ...] = (),
|
|
1031
|
+
live: LiveOptions | None = None,
|
|
1032
|
+
scoped_flags: dict[str, tuple[bool, tuple[str, ...]]] | None = None,
|
|
1033
|
+
) -> None:
|
|
1034
|
+
"""A positional ID belongs to `add` alone and `--sku` to `zoo`: accepting
|
|
1035
|
+
either elsewhere would silently ignore what the caller typed."""
|
|
1036
|
+
if model_id is not None and subcommand not in ("add", "prep", "run", "ab"):
|
|
1037
|
+
raise ModelError(
|
|
1038
|
+
"model.unexpected-argument",
|
|
1039
|
+
f"`tan model {subcommand}` takes no ID argument (got {model_id}); only `add`, `prep`, `run` and `ab` do.",
|
|
1040
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1041
|
+
)
|
|
1042
|
+
if device_flags:
|
|
1043
|
+
device, capture, against_capture, has_input, has_runs = device_flags
|
|
1044
|
+
if (device or capture or against_capture) and subcommand not in ("run", "ab"):
|
|
1045
|
+
raise ModelError(
|
|
1046
|
+
"model.unexpected-argument",
|
|
1047
|
+
f"`tan model {subcommand}` takes no --device/--capture/--against-capture.",
|
|
1048
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1049
|
+
)
|
|
1050
|
+
if (capture or against_capture) and not device:
|
|
1051
|
+
raise ModelError(
|
|
1052
|
+
"model.unexpected-argument",
|
|
1053
|
+
"--capture/--against-capture need --device.",
|
|
1054
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1055
|
+
)
|
|
1056
|
+
if against_capture and subcommand != "ab":
|
|
1057
|
+
raise ModelError(
|
|
1058
|
+
"model.unexpected-argument",
|
|
1059
|
+
"--against-capture belongs to `ab --device`.",
|
|
1060
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1061
|
+
)
|
|
1062
|
+
if live is not None and live.any_set() and not (device and not (capture or against_capture)):
|
|
1063
|
+
raise ModelError(
|
|
1064
|
+
"model.unexpected-argument",
|
|
1065
|
+
"--core/--wait/--confirm/--probe-*/--jlink/--against-project apply to a live "
|
|
1066
|
+
"`run`/`ab --device` (no --capture).",
|
|
1067
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1068
|
+
)
|
|
1069
|
+
if device and (has_input or has_runs):
|
|
1070
|
+
raise ModelError(
|
|
1071
|
+
"model.unexpected-argument",
|
|
1072
|
+
"--input and --runs apply to host runs; a device run times what the target ran.",
|
|
1073
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1074
|
+
)
|
|
1075
|
+
# tan-cli#1486: subcommand-scoped flags typed on the wrong subcommand were
|
|
1076
|
+
# accepted and dropped, so the caller got no signal the flag did nothing.
|
|
1077
|
+
for flag, (given, owners) in (scoped_flags or {}).items():
|
|
1078
|
+
if given and subcommand not in owners:
|
|
1079
|
+
raise ModelError(
|
|
1080
|
+
"model.unexpected-argument",
|
|
1081
|
+
f"`tan model {subcommand}` takes no {flag}; it applies to "
|
|
1082
|
+
f"{', '.join('`' + o + '`' for o in owners)} only.",
|
|
1083
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1084
|
+
)
|
|
1085
|
+
if sku and subcommand == "add":
|
|
1086
|
+
raise ModelError(
|
|
1087
|
+
"model.unexpected-argument",
|
|
1088
|
+
"`tan model add` takes no --sku; the board's own som.sku is what it checks.",
|
|
1089
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _empty_data(subcommand: str | None) -> dict[str, Any]:
|
|
1094
|
+
"""The `data` shape for a refusal that never reached `_run_build`/
|
|
1095
|
+
`_run_doctor`/`_run_check`/`_run_list` -- each subcommand's OWN empty
|
|
1096
|
+
payload shape, not a generic stand-in, so a consumer parsing
|
|
1097
|
+
`data.backends`/`data.built`/`data.models` off a refusal envelope gets the
|
|
1098
|
+
same shape it would from a run that resolved nothing."""
|
|
1099
|
+
if subcommand == "doctor":
|
|
1100
|
+
return {"schemaVersion": DOCTOR_DATA_SCHEMA_VERSION, "backends": [], "optional": []}
|
|
1101
|
+
if subcommand == "check":
|
|
1102
|
+
return {"schemaVersion": CHECK_DATA_SCHEMA_VERSION, "sku": None, "exact": False, "models": []}
|
|
1103
|
+
if subcommand == "list":
|
|
1104
|
+
return {"schemaVersion": LIST_DATA_SCHEMA_VERSION, "sku": None, "models": []}
|
|
1105
|
+
if subcommand == "zoo":
|
|
1106
|
+
return zoo_empty_data()
|
|
1107
|
+
if subcommand == "add":
|
|
1108
|
+
return add_empty_data()
|
|
1109
|
+
if subcommand == "prep":
|
|
1110
|
+
return prep_empty_data()
|
|
1111
|
+
if subcommand == "run":
|
|
1112
|
+
return run_empty_data()
|
|
1113
|
+
if subcommand == "ab":
|
|
1114
|
+
return ab_empty_data()
|
|
1115
|
+
return {"schemaVersion": DATA_SCHEMA_VERSION, "sku": None, "built": []}
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def model(
|
|
1119
|
+
subcommand: str = typer.Argument(
|
|
1120
|
+
None, metavar="SUBCOMMAND", help="build | doctor | check | list | zoo | add | prep | run | ab."
|
|
1121
|
+
),
|
|
1122
|
+
board: str = typer.Option(
|
|
1123
|
+
# tan-cli#398: `--board-yaml` is a REAL second spelling of this one
|
|
1124
|
+
# option, not a second option -- it is what `build`, `run`, `kconfig`,
|
|
1125
|
+
# `validate`, `generate` and `inspect` all call the board file, and
|
|
1126
|
+
# what alp-sdk-vscode's `docs/CLI.md` "Common flags" tells a caller every command
|
|
1127
|
+
# supports. Declared here so `accept_global_flags` sees it as already
|
|
1128
|
+
# covered and stops injecting an inert twin behind this one: a caller
|
|
1129
|
+
# who used the surface-wide spelling was served `./board.yaml`'s
|
|
1130
|
+
# `som.sku` instead of the file they named, and `som.sku` is what
|
|
1131
|
+
# picks the silicon `build_model` compiles for.
|
|
1132
|
+
"board.yaml", "--board", "--board-yaml", metavar="PATH", help="Path to board.yaml."
|
|
1133
|
+
),
|
|
1134
|
+
out: str = typer.Option(
|
|
1135
|
+
None, "--out", metavar="PATH", help="Output directory (default build/models; `prep`: build/model-prep)."
|
|
1136
|
+
),
|
|
1137
|
+
metadata_root: str = typer.Option(
|
|
1138
|
+
None,
|
|
1139
|
+
"--metadata-root",
|
|
1140
|
+
metavar="PATH",
|
|
1141
|
+
help="Path to the metadata/ root (default: <sdk-root>/metadata).",
|
|
1142
|
+
),
|
|
1143
|
+
project: str = typer.Option(
|
|
1144
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
1145
|
+
),
|
|
1146
|
+
sdk_root: str = typer.Option(
|
|
1147
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
1148
|
+
),
|
|
1149
|
+
exact: bool = typer.Option(
|
|
1150
|
+
False,
|
|
1151
|
+
"--exact",
|
|
1152
|
+
help="With `check`: attempt a real compile (Ethos-U only, via `vela`) "
|
|
1153
|
+
"instead of the static screen. Refused by the other subcommands.",
|
|
1154
|
+
),
|
|
1155
|
+
sku: str = typer.Option(
|
|
1156
|
+
None,
|
|
1157
|
+
"--sku",
|
|
1158
|
+
metavar="SKU",
|
|
1159
|
+
help="With `zoo`: only entries bench-validated on this SoM SKU. Ignored by the others.",
|
|
1160
|
+
),
|
|
1161
|
+
output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
|
|
1162
|
+
calibration: str = typer.Option(
|
|
1163
|
+
None, "--calibration", metavar="DIR", help="With `prep`: directory of .npy calibration samples."
|
|
1164
|
+
),
|
|
1165
|
+
per_channel: bool = typer.Option(
|
|
1166
|
+
False, "--per-channel", help="With `prep`: per-channel weight quantization."
|
|
1167
|
+
),
|
|
1168
|
+
min_samples: int | None = typer.Option(
|
|
1169
|
+
None, "--min-samples", metavar="N",
|
|
1170
|
+
help="With `prep`: fewest calibration samples accepted (default 8)."
|
|
1171
|
+
),
|
|
1172
|
+
device: bool = typer.Option(
|
|
1173
|
+
False,
|
|
1174
|
+
"--device",
|
|
1175
|
+
help="With `run`/`ab`: report the on-device tier, from --capture FILE or live (RAM-run the "
|
|
1176
|
+
"built diagnostics.link: itcm project, needs --confirm).",
|
|
1177
|
+
),
|
|
1178
|
+
core: str = typer.Option(
|
|
1179
|
+
"m55_he", "--core", metavar="CORE_ID", help="With live `--device`: the ITCM-linked slice to RAM-run."
|
|
1180
|
+
),
|
|
1181
|
+
wait: float = typer.Option(
|
|
1182
|
+
1.5, "--wait", metavar="SECONDS", min=0.0, max=3600.0,
|
|
1183
|
+
help="With live `--device`: seconds the image runs before the RAM console is read.",
|
|
1184
|
+
),
|
|
1185
|
+
confirm: bool = typer.Option(
|
|
1186
|
+
False, "--confirm",
|
|
1187
|
+
help="With live `--device`: arm the RAM-run (resets the whole device; never writes MRAM). "
|
|
1188
|
+
"Armed by --confirm, ALP_FLASH_FORCE=1 or the manifest's flash_args.confirm: true.",
|
|
1189
|
+
),
|
|
1190
|
+
probe_serial: str = typer.Option(None, "--probe-serial", metavar="SN", help="With live `--device`: J-Link serial."),
|
|
1191
|
+
probe_usb_path: str = typer.Option(
|
|
1192
|
+
None, "--probe-usb-path", metavar="BUS-PORT", help="With live `--device`: J-Link USB port path (e.g. 3-4.2)."
|
|
1193
|
+
),
|
|
1194
|
+
jlink: str = typer.Option(None, "--jlink", metavar="PATH", help="With live `--device`: J-Link Commander binary."),
|
|
1195
|
+
against_project: str = typer.Option(
|
|
1196
|
+
None, "--against-project", metavar="PATH", help="With live `ab --device`: B's project root (A is --project); B uses its own board.yaml."
|
|
1197
|
+
),
|
|
1198
|
+
capture: str = typer.Option(
|
|
1199
|
+
None, "--capture", metavar="FILE", help="With `--device`: the target's console capture."
|
|
1200
|
+
),
|
|
1201
|
+
against_capture: str = typer.Option(
|
|
1202
|
+
None, "--against-capture", metavar="FILE", help="With `ab --device`: B's console capture."
|
|
1203
|
+
),
|
|
1204
|
+
against: str = typer.Option(
|
|
1205
|
+
None, "--against", metavar="PATH", help="With `ab`: the second .onnx model to compare with."
|
|
1206
|
+
),
|
|
1207
|
+
runs: int = typer.Option(None, "--runs", metavar="N", help="With host `run`/`ab`: timed inferences per model (default 20)."),
|
|
1208
|
+
input_file: str = typer.Option(
|
|
1209
|
+
None, "--input", metavar="PATH", help="With `run`/`ab`: a .npy input sample (default: seeded random)."
|
|
1210
|
+
),
|
|
1211
|
+
model_id: str = typer.Argument(
|
|
1212
|
+
None,
|
|
1213
|
+
metavar="ID",
|
|
1214
|
+
help="With `add`: the model-zoo entry id (see `zoo`). With `prep`/`run`/`ab`: the .onnx model file.",
|
|
1215
|
+
),
|
|
1216
|
+
) -> None:
|
|
1217
|
+
"""Compile + package board.yaml `models:` into `.alpmodel` packages
|
|
1218
|
+
(`build`), report NPU-compiler toolchain availability (`doctor`),
|
|
1219
|
+
statically screen a declared model's NPU eligibility (`check`), or list
|
|
1220
|
+
what is declared next to what is already built (`list`), list the SDK's model
|
|
1221
|
+
zoo (`zoo`), add a zoo model to the project (`add <id>`), INT8-quantize an
|
|
1222
|
+
ONNX model with an accuracy report (`prep`), or time a host reference
|
|
1223
|
+
run (`run`) / compare two models (`ab`)."""
|
|
1224
|
+
json_mode = output_format == "json"
|
|
1225
|
+
out = out or ("build/model-prep" if subcommand == "prep" else "build/models")
|
|
1226
|
+
|
|
1227
|
+
def finish(
|
|
1228
|
+
project_: Project,
|
|
1229
|
+
sdk: SdkInfo | None,
|
|
1230
|
+
data: dict,
|
|
1231
|
+
issues: list[Issue],
|
|
1232
|
+
exit_code: ExitCode,
|
|
1233
|
+
) -> None:
|
|
1234
|
+
if json_mode:
|
|
1235
|
+
emit(Envelope("model", project_, data, issues, exit_code, sdk=sdk))
|
|
1236
|
+
else:
|
|
1237
|
+
# tan-cli#497 defect 3: warnings LEAD. `issues` now carries the
|
|
1238
|
+
# SDK-resolution pair, which is a fact about the whole run --
|
|
1239
|
+
# which checkout answered -- not a per-model outcome, so it must
|
|
1240
|
+
# be readable before the result lines rather than buried under
|
|
1241
|
+
# them. `{severity}: {message}`, the shape `build`/`run`/`kconfig`
|
|
1242
|
+
# print a resolution warning with, so the same workspace reads
|
|
1243
|
+
# the same way whichever command a developer runs.
|
|
1244
|
+
warnings = [i for i in issues if i.severity != "error"]
|
|
1245
|
+
errors = [i for i in issues if i.severity == "error"]
|
|
1246
|
+
for issue in warnings:
|
|
1247
|
+
print(f"{issue.severity}: {issue.message}", file=sys.stderr)
|
|
1248
|
+
if "backends" in data:
|
|
1249
|
+
# `doctor`: one line per backend, in registry order --
|
|
1250
|
+
# unavailable rows carry their reason inline, so a scrollback
|
|
1251
|
+
# answers "why not" without a second command.
|
|
1252
|
+
for row in data["backends"]:
|
|
1253
|
+
tool = row["tool"] or "-"
|
|
1254
|
+
if row["available"]:
|
|
1255
|
+
version = f", version={row['version']}" if row["version"] else ""
|
|
1256
|
+
print(
|
|
1257
|
+
f"{row['backend']}: available (tool={tool}{version})",
|
|
1258
|
+
file=sys.stderr,
|
|
1259
|
+
)
|
|
1260
|
+
else:
|
|
1261
|
+
# A backend with no `_UNAVAILABLE_REASONS` entry (and
|
|
1262
|
+
# no doctor-side caveat) carries `reason: None` --
|
|
1263
|
+
# drop the `-- ...` clause entirely rather than
|
|
1264
|
+
# rendering the Python literal `None` into the line.
|
|
1265
|
+
suffix = f" -- {row['reason']}" if row["reason"] else ""
|
|
1266
|
+
print(
|
|
1267
|
+
f"{row['backend']}: unavailable (tool={tool}){suffix}",
|
|
1268
|
+
file=sys.stderr,
|
|
1269
|
+
)
|
|
1270
|
+
# OPTIONAL prerequisites, after the backend rows and never
|
|
1271
|
+
# spelled `unavailable`: the word this branch leads with is
|
|
1272
|
+
# what stops a licensed-only enhancement reading as a fault
|
|
1273
|
+
# in a scrollback (`tan.core.model_doctor.optional_row`).
|
|
1274
|
+
for row in data.get("optional", []):
|
|
1275
|
+
tool = row["tool"] or "-"
|
|
1276
|
+
if row["available"]:
|
|
1277
|
+
print(
|
|
1278
|
+
f"{row['backend']}: optional (tool={tool}) present",
|
|
1279
|
+
file=sys.stderr,
|
|
1280
|
+
)
|
|
1281
|
+
else:
|
|
1282
|
+
suffix = f" -- {row['reason']}" if row["reason"] else ""
|
|
1283
|
+
print(
|
|
1284
|
+
f"{row['backend']}: optional (tool={tool}) not in use{suffix}",
|
|
1285
|
+
file=sys.stderr,
|
|
1286
|
+
)
|
|
1287
|
+
elif subcommand == "zoo":
|
|
1288
|
+
if not data.get("entries") and not errors:
|
|
1289
|
+
print("model zoo: no matching entries.", file=sys.stderr)
|
|
1290
|
+
for line in render_zoo_text(data):
|
|
1291
|
+
print(line, file=sys.stderr)
|
|
1292
|
+
elif subcommand == "add":
|
|
1293
|
+
for line in render_add_text(data):
|
|
1294
|
+
print(line, file=sys.stderr)
|
|
1295
|
+
elif subcommand in ("prep", "run", "ab"):
|
|
1296
|
+
render = {"prep": render_prep_text, "run": render_run_text, "ab": render_ab_text}
|
|
1297
|
+
for line in render[subcommand](data):
|
|
1298
|
+
print(line, file=sys.stderr)
|
|
1299
|
+
elif subcommand == "list":
|
|
1300
|
+
# `list`: checked by SUBCOMMAND, not by `"models" in data` --
|
|
1301
|
+
# `check`'s payload carries that same key, so shape alone
|
|
1302
|
+
# cannot tell the two apart (tan-cli#674). Rendered from the
|
|
1303
|
+
# SAME serialised `data` dict `--format json` emits
|
|
1304
|
+
# (`tan.core.model_list.render_list_text`), the identical
|
|
1305
|
+
# one-source-two-renderers split `doctor`/`check` above use.
|
|
1306
|
+
if not data.get("models") and not errors:
|
|
1307
|
+
print(
|
|
1308
|
+
"model list: no `models:` declared in board.yaml; nothing to list.",
|
|
1309
|
+
file=sys.stderr,
|
|
1310
|
+
)
|
|
1311
|
+
for line in render_list_text(data):
|
|
1312
|
+
print(line, file=sys.stderr)
|
|
1313
|
+
elif "models" in data:
|
|
1314
|
+
# `check`: rendered from the SAME serialised `data` dict
|
|
1315
|
+
# `--format json` emits (`tan.core.model_check.render_check_text`),
|
|
1316
|
+
# the identical one-source-two-renderers split `doctor`'s own
|
|
1317
|
+
# branch above already uses.
|
|
1318
|
+
if not data.get("models") and not errors:
|
|
1319
|
+
print(
|
|
1320
|
+
"model check: no `models:` declared in board.yaml; nothing to check.",
|
|
1321
|
+
file=sys.stderr,
|
|
1322
|
+
)
|
|
1323
|
+
for line in render_check_text(data):
|
|
1324
|
+
print(line, file=sys.stderr)
|
|
1325
|
+
else:
|
|
1326
|
+
for path in data.get("built", []):
|
|
1327
|
+
print(f"built {path}", file=sys.stderr)
|
|
1328
|
+
# `not issues` before -- the same thing until `issues` started
|
|
1329
|
+
# carrying those warnings. A board with no `models:` and a
|
|
1330
|
+
# broken project pin must still be told nothing was declared;
|
|
1331
|
+
# a warning is not a reason to withhold the only line this
|
|
1332
|
+
# run had to say.
|
|
1333
|
+
if not data.get("built") and not errors:
|
|
1334
|
+
print(
|
|
1335
|
+
"model: no `models:` declared in board.yaml; nothing to build.",
|
|
1336
|
+
file=sys.stderr,
|
|
1337
|
+
)
|
|
1338
|
+
for issue in errors:
|
|
1339
|
+
print(f"model: {issue.message}", file=sys.stderr)
|
|
1340
|
+
raise typer.Exit(int(exit_code))
|
|
1341
|
+
|
|
1342
|
+
if subcommand not in SUBCOMMANDS:
|
|
1343
|
+
finish(
|
|
1344
|
+
Project(root=None, board_yaml=None),
|
|
1345
|
+
None,
|
|
1346
|
+
_empty_data(subcommand),
|
|
1347
|
+
[
|
|
1348
|
+
Issue(
|
|
1349
|
+
"model.unknown-subcommand",
|
|
1350
|
+
"error",
|
|
1351
|
+
f"Unknown model subcommand: {'(none)' if subcommand is None else subcommand}. "
|
|
1352
|
+
f"Available: {', '.join(SUBCOMMANDS)}.",
|
|
1353
|
+
)
|
|
1354
|
+
],
|
|
1355
|
+
ExitCode.RUNTIME_FAILURE,
|
|
1356
|
+
)
|
|
1357
|
+
return
|
|
1358
|
+
|
|
1359
|
+
# tan-cli#497 defect 3: `model build` was the only `resolve_project_context`
|
|
1360
|
+
# caller that read `.workspace_root`/`.board_yaml`/`.project()`/`.sdk` off
|
|
1361
|
+
# the returned context and NONE of the three resolution facts -- so it
|
|
1362
|
+
# alone dropped BOTH `sdk.project-pin-unresolved` (tan-cli#263) and
|
|
1363
|
+
# `sdk.global-default-foreign-project` (tan-cli#464), in JSON and text
|
|
1364
|
+
# alike, while `size` and `image` reported them from the same directory
|
|
1365
|
+
# through the same resolver. That matters most here: the `.alpmodel`
|
|
1366
|
+
# packages are compiled against `<resolved checkout>/metadata`, i.e.
|
|
1367
|
+
# against that checkout's target/backend table for `som.sku`.
|
|
1368
|
+
#
|
|
1369
|
+
# Resolved HERE rather than inside `_run_build` so the warnings survive a
|
|
1370
|
+
# `ModelError` too -- `project_pin_issue`'s contract is "shared by EVERY
|
|
1371
|
+
# caller of `resolve_sdk_tiered`", not "every caller that got as far as a
|
|
1372
|
+
# successful build".
|
|
1373
|
+
sdk_issues: list[Issue] = []
|
|
1374
|
+
try:
|
|
1375
|
+
context = resolve_project_context(project, board, sdk_root)
|
|
1376
|
+
sdk_issues = sdk_resolution_issues(
|
|
1377
|
+
context.broken_project_pin,
|
|
1378
|
+
context.sdk_source_tier,
|
|
1379
|
+
context.foreign_global_default_for,
|
|
1380
|
+
)
|
|
1381
|
+
live = LiveOptions(
|
|
1382
|
+
core=core if isinstance(core, str) else "m55_he",
|
|
1383
|
+
wait=float(wait) if isinstance(wait, (int, float)) else 1.5,
|
|
1384
|
+
confirm=confirm is True,
|
|
1385
|
+
probe_serial=probe_serial if isinstance(probe_serial, str) else None,
|
|
1386
|
+
probe_usb_path=probe_usb_path if isinstance(probe_usb_path, str) else None,
|
|
1387
|
+
jlink=jlink if isinstance(jlink, str) else None,
|
|
1388
|
+
sdk_root=sdk_root,
|
|
1389
|
+
against_project=against_project if isinstance(against_project, str) else None,
|
|
1390
|
+
board_yaml=board if isinstance(board, str) and board != "board.yaml" else None,
|
|
1391
|
+
)
|
|
1392
|
+
_refuse_stray_arguments(
|
|
1393
|
+
subcommand, model_id, sku, (device, bool(capture), bool(against_capture), input_file is not None, runs is not None),
|
|
1394
|
+
live,
|
|
1395
|
+
{
|
|
1396
|
+
"--against": (against is not None, ("ab",)),
|
|
1397
|
+
"--against-project": (against_project is not None, ("ab",)),
|
|
1398
|
+
"--calibration": (calibration is not None, ("prep",)),
|
|
1399
|
+
"--per-channel": (per_channel is True, ("prep",)),
|
|
1400
|
+
"--min-samples": (min_samples is not None, ("prep",)),
|
|
1401
|
+
"--exact": (exact is True, ("check",)),
|
|
1402
|
+
},
|
|
1403
|
+
)
|
|
1404
|
+
if subcommand == "doctor":
|
|
1405
|
+
project_, sdk, data, issues, exit_code = _run_doctor(
|
|
1406
|
+
context=context,
|
|
1407
|
+
sdk_root=sdk_root,
|
|
1408
|
+
)
|
|
1409
|
+
elif subcommand == "check":
|
|
1410
|
+
project_, sdk, data, issues, exit_code = _run_check(
|
|
1411
|
+
context=context,
|
|
1412
|
+
metadata_root=metadata_root,
|
|
1413
|
+
sdk_root=sdk_root,
|
|
1414
|
+
exact=exact,
|
|
1415
|
+
)
|
|
1416
|
+
elif subcommand == "run" and device:
|
|
1417
|
+
project_, sdk, data, issues, exit_code = run_device_run(
|
|
1418
|
+
context=context, source=model_id, capture=capture, live=live
|
|
1419
|
+
)
|
|
1420
|
+
elif subcommand == "ab" and device:
|
|
1421
|
+
project_, sdk, data, issues, exit_code = run_device_ab(
|
|
1422
|
+
context=context, source=model_id, against=against, capture=capture,
|
|
1423
|
+
against_capture=against_capture, live=live,
|
|
1424
|
+
)
|
|
1425
|
+
elif subcommand == "run":
|
|
1426
|
+
project_, sdk, data, issues, exit_code = run_run(
|
|
1427
|
+
context=context, source=model_id, runs=20 if runs is None else runs, input_file=input_file
|
|
1428
|
+
)
|
|
1429
|
+
elif subcommand == "ab":
|
|
1430
|
+
project_, sdk, data, issues, exit_code = run_ab(
|
|
1431
|
+
context=context, source=model_id, against=against, runs=20 if runs is None else runs, input_file=input_file
|
|
1432
|
+
)
|
|
1433
|
+
elif subcommand == "prep":
|
|
1434
|
+
project_, sdk, data, issues, exit_code = run_prep(
|
|
1435
|
+
context=context,
|
|
1436
|
+
source=model_id,
|
|
1437
|
+
calibration=calibration,
|
|
1438
|
+
out=out,
|
|
1439
|
+
per_channel=per_channel,
|
|
1440
|
+
min_samples=8 if min_samples is None else min_samples,
|
|
1441
|
+
)
|
|
1442
|
+
elif subcommand in ("zoo", "add"):
|
|
1443
|
+
resolved = _require_metadata_sdk_root(sdk_root, context.workspace_root, "No zoo was read.")
|
|
1444
|
+
zoo_dir = _resolve_metadata_dir(metadata_root, resolved, Path(context.workspace_root)) / "model_zoo"
|
|
1445
|
+
if subcommand == "zoo":
|
|
1446
|
+
project_, sdk, data, issues, exit_code = run_zoo(context=context, zoo_dir=zoo_dir, sku=sku)
|
|
1447
|
+
else:
|
|
1448
|
+
board_doc = _load_board(Path(context.board_yaml))
|
|
1449
|
+
models_ = _require_models_list(board_doc, Path(context.board_yaml))
|
|
1450
|
+
declared = {m["name"] for m in models_ if isinstance(m, dict) and "name" in m}
|
|
1451
|
+
project_, sdk, data, issues, exit_code = run_add(
|
|
1452
|
+
context=context,
|
|
1453
|
+
zoo_dir=zoo_dir,
|
|
1454
|
+
model_id=model_id,
|
|
1455
|
+
sku=declared_sku(board_doc),
|
|
1456
|
+
existing_names=declared,
|
|
1457
|
+
)
|
|
1458
|
+
elif subcommand == "list":
|
|
1459
|
+
project_, sdk, data, issues, exit_code = _run_list(
|
|
1460
|
+
context=context,
|
|
1461
|
+
out=out,
|
|
1462
|
+
)
|
|
1463
|
+
else:
|
|
1464
|
+
project_, sdk, data, issues, exit_code = _run_build(
|
|
1465
|
+
context=context,
|
|
1466
|
+
out=out,
|
|
1467
|
+
metadata_root=metadata_root,
|
|
1468
|
+
sdk_root=sdk_root,
|
|
1469
|
+
)
|
|
1470
|
+
except ModelError as err:
|
|
1471
|
+
finish(
|
|
1472
|
+
Project(root=None, board_yaml=None),
|
|
1473
|
+
None,
|
|
1474
|
+
_empty_data(subcommand),
|
|
1475
|
+
[*sdk_issues, Issue(err.code, "error", err.message)],
|
|
1476
|
+
err.exit_code,
|
|
1477
|
+
)
|
|
1478
|
+
return
|
|
1479
|
+
except Exception as err: # noqa: BLE001 -- the envelope IS the error contract
|
|
1480
|
+
finish(
|
|
1481
|
+
Project(root=None, board_yaml=None),
|
|
1482
|
+
None,
|
|
1483
|
+
_empty_data(subcommand),
|
|
1484
|
+
[
|
|
1485
|
+
*sdk_issues,
|
|
1486
|
+
Issue(
|
|
1487
|
+
"model.internal-failure",
|
|
1488
|
+
"error",
|
|
1489
|
+
f"model {subcommand} failed unexpectedly: {type(err).__name__}: {err}",
|
|
1490
|
+
),
|
|
1491
|
+
],
|
|
1492
|
+
ExitCode.INTERNAL_FAILURE,
|
|
1493
|
+
)
|
|
1494
|
+
return
|
|
1495
|
+
|
|
1496
|
+
finish(project_, sdk, data, [*sdk_issues, *issues], exit_code)
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
# tan-cli#261: adds the seven oracle `GlobalArgs` flags this command is
|
|
1500
|
+
# missing entirely (`--all`/`--ci`/`--no-color`/`--non-interactive`/
|
|
1501
|
+
# `--quiet`/`--target`/`--verbose`); see `tan.core.global_flags`.
|
|
1502
|
+
#
|
|
1503
|
+
# `--board-yaml` is NOT among them any more (tan-cli#398). It used to be, and
|
|
1504
|
+
# the note here claimed that was harmless because "`model`'s own `--board`
|
|
1505
|
+
# already plays `--board-yaml`'s role for real" -- but the two spellings were
|
|
1506
|
+
# never wired together, so the injected one was accepted, dropped, and the
|
|
1507
|
+
# default `./board.yaml` packaged instead of the file the caller named, at
|
|
1508
|
+
# `exitCode: 0` with `issues: []`. It is now a second decl on `--board`
|
|
1509
|
+
# itself (above), so both spellings are one option and read one file.
|
|
1510
|
+
#
|
|
1511
|
+
# The six arity-0 flags above stay accepted-and-dropped, and so does the
|
|
1512
|
+
# injected `--target` (the refusal was reverted per tan-cli#403; see
|
|
1513
|
+
# `core/global_flags.py`) -- `model` has no emit target to honour it with.
|
|
1514
|
+
model = accept_global_flags(model)
|