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
tan/model/check.py
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan model check`'s engine glue -- which backends a SKU screens against,
|
|
3
|
+
and the opportunistic `--exact` upgrade (ADR-0028 amendment, tan-cli#782
|
|
4
|
+
Tasks 4/6).
|
|
5
|
+
|
|
6
|
+
`tan.model.analyze` is the pure static-eligibility screen and deliberately
|
|
7
|
+
knows nothing about a SKU's *actual* NPU backends or about a real compiler --
|
|
8
|
+
its own docstring says so ("no click/typer, no envelope construction").
|
|
9
|
+
This module is the layer above it that a `check` run actually calls:
|
|
10
|
+
|
|
11
|
+
* [`resolve_check_backends`] -- which non-cpu backends @sku's SoM declares
|
|
12
|
+
at all (`tan.model.targets.resolve_targets`, the SAME resolver
|
|
13
|
+
`build_model` uses to decide what to compile), so `check` never screens
|
|
14
|
+
a model against hardware the board doesn't have -- an Ethos-U row on a
|
|
15
|
+
V2N board would misreport hardware that isn't there.
|
|
16
|
+
* [`check_model_backends`] -- one `BackendReport` per resolved backend for
|
|
17
|
+
one model source, walking its ops once (`tensorio.extract_ops`) and
|
|
18
|
+
reusing them across every backend.
|
|
19
|
+
* The `--exact` upgrade: Vela is a free, un-gated `pip install`
|
|
20
|
+
(`ethos-u-vela`), so for `ethos_u` the "offline, no toolchain" constraint
|
|
21
|
+
the static screen exists for is soft. When `--exact` is set and `vela` is
|
|
22
|
+
on PATH, [`_exact_ethos_u`] runs the REAL compiler; [`_report_from_
|
|
23
|
+
vela_compile`] then reads what vela ACTUALLY placed on the NPU (`Blob.
|
|
24
|
+
npu_op_count`/`cpu_op_count`, sourced from vela's own "NPU operators = N
|
|
25
|
+
(P%)" summary line) and only returns `basis: "compiled"` / `npu_coverage:
|
|
26
|
+
"fits"` -- the only basis allowed to say `"fits"`
|
|
27
|
+
(`analyze.BackendReport.npu_coverage`'s own comment) -- at 100% NPU
|
|
28
|
+
placement. A clean vela exit is NOT proof of that: vela exits 0 on a full
|
|
29
|
+
CPU fallback by design (measured: a float32 FULLY_CONNECTED model prints
|
|
30
|
+
"NPU operators = 0 (0.0%)" and still exits 0), so partial/zero placement
|
|
31
|
+
reports the real split (`npu_coverage: "partial"`/`"cpu-only"`) and keeps
|
|
32
|
+
the static per-op verdicts (`report.ops`) rather than discarding them.
|
|
33
|
+
`drpai`/`deepx_dxm1` stay opportunistic-only: both toolchains are
|
|
34
|
+
license-gated (`dxcom`, the DRP-AI TVM checkout), so `--exact` never
|
|
35
|
+
attempts to spawn either here -- [`_license_gated_exact_note`] reports
|
|
36
|
+
that as a reason, not a crash, exactly like `model doctor`'s own narrower
|
|
37
|
+
probes do for the same two backends.
|
|
38
|
+
* The tier-2 upgrade: [`tan.model.perf_apply.apply_perf_point`] swaps in a
|
|
39
|
+
bench-measured `metadata/model_perf/` point (`tan.model.perf` reads it,
|
|
40
|
+
`tan.model.perf_apply` matches + applies it) when alp-sdk publishes one
|
|
41
|
+
for this EXACT identity -- the `basis: "bench"` half of the resolution
|
|
42
|
+
order below. Split into its own module (tan-cli#791 review NIT (c)): it is
|
|
43
|
+
pure string/decision logic with no subprocess in it, and this file was
|
|
44
|
+
against the house module-size cap.
|
|
45
|
+
|
|
46
|
+
RESOLUTION ORDER IS **precomputed -> exact-if-toolchain -> static**, and it is
|
|
47
|
+
not the order the code RUNS in. `check_model_backends` computes the static
|
|
48
|
+
screen first because everything else is built on top of it, then runs
|
|
49
|
+
`--exact` (only when the customer asked, and only when vela is really there),
|
|
50
|
+
then consults the perf point LAST -- but the point is what WINS, because it is
|
|
51
|
+
a measurement off real silicon rather than a screen or a host compile. The one
|
|
52
|
+
exception is decision 1 of the tier-2 plan, and it is deliberate: when the
|
|
53
|
+
customer really does hold the toolchain and their own compile DISAGREES with
|
|
54
|
+
our bench point, `--exact` wins, because their profile may not be ours and the
|
|
55
|
+
number they can act on is the one their machine produces
|
|
56
|
+
(`tan.model.perf_apply._perf_disagreements` defines "disagrees" concretely, on
|
|
57
|
+
the three figures both sides actually report).
|
|
58
|
+
|
|
59
|
+
Does its own IO (reads `metadata/**` through `resolve_targets`/
|
|
60
|
+
`resolve_ethos_u_variant`, and `--exact` shells `vela`) -- the same stance
|
|
61
|
+
`tan.model.build`/`tan.model.targets` already take: IO lives in `tan.model`,
|
|
62
|
+
never in `tan.core` (see `tan.core.model_check`, which is the pure
|
|
63
|
+
serialisation/rendering half `tan.commands.model_cmd` calls alongside this).
|
|
64
|
+
"""
|
|
65
|
+
from __future__ import annotations
|
|
66
|
+
|
|
67
|
+
import shutil
|
|
68
|
+
import tempfile
|
|
69
|
+
from dataclasses import replace
|
|
70
|
+
from pathlib import Path
|
|
71
|
+
|
|
72
|
+
from .adapters import Blob
|
|
73
|
+
from .adapters.ethos_u import VelaAdapter, VelaFootprintRefused
|
|
74
|
+
from .build import _placed_nothing_on_accelerator
|
|
75
|
+
from .analyze import BackendReport, analyze_backend, resolve_ethos_u_variant
|
|
76
|
+
from .perf import has_perf_points
|
|
77
|
+
from .perf_apply import (
|
|
78
|
+
_model_sha256,
|
|
79
|
+
_placement_outcome,
|
|
80
|
+
_resolve_hw_rev,
|
|
81
|
+
apply_perf_point,
|
|
82
|
+
)
|
|
83
|
+
from .sram_fit import SramFit, evaluate_sram_fit
|
|
84
|
+
from .targets import TargetSpec, resolve_targets
|
|
85
|
+
from .tensorio import extract_ops, tflite_reader_available
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def resolve_check_backends(sku: str, *, metadata_root: Path) -> list[str]:
|
|
89
|
+
"""Distinct non-`cpu` backend names @sku's SoM actually declares, in
|
|
90
|
+
`resolve_targets`' own first-seen order -- the exact set `check` screens
|
|
91
|
+
a model against. `cpu` is excluded: "how much of this model can target
|
|
92
|
+
the NPU" has nothing to say about the fallback CPU path itself.
|
|
93
|
+
|
|
94
|
+
This exclusion is also why tier 2 (`tan.model.perf_apply._PERF_TOOLCHAIN`)
|
|
95
|
+
never asks for a `cpu` perf point either, even though alp-sdk's schema
|
|
96
|
+
allows publishing one (tan-cli#791 review item 6): `check_model_backends`
|
|
97
|
+
only ever iterates the backends THIS function returns, so a `cpu` point
|
|
98
|
+
would have no caller to be handed to regardless of what
|
|
99
|
+
`_PERF_TOOLCHAIN` said. See that dict's own comment for the fuller
|
|
100
|
+
campaign-planner-facing statement of the decision.
|
|
101
|
+
|
|
102
|
+
Raises exactly what `resolve_targets` raises (`FileNotFoundError` for no
|
|
103
|
+
SoM preset or no SoC spec, `ValueError` for a malformed `silicon:` ref) --
|
|
104
|
+
a board-level fact about the SKU, not a per-model one, so the caller
|
|
105
|
+
reports it once for the whole run rather than once per declared model.
|
|
106
|
+
"""
|
|
107
|
+
seen: list[str] = []
|
|
108
|
+
for spec in resolve_targets(sku, metadata_root=metadata_root):
|
|
109
|
+
if spec.backend != "cpu" and spec.backend not in seen:
|
|
110
|
+
seen.append(spec.backend)
|
|
111
|
+
return seen
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# The perf-point step below is gated on `has_perf_points` and its digest is
|
|
115
|
+
# computed LAZILY behind that gate, once for all backends: no alp-sdk has
|
|
116
|
+
# published a `metadata/model_perf/` tree yet, and hashing every model source on
|
|
117
|
+
# every `check` to look inside a directory that does not exist would be pure cost
|
|
118
|
+
# for every customer alive today.
|
|
119
|
+
#
|
|
120
|
+
# `hw_rev` is the customer's own `board.yaml` `som.hw_rev`, or `None` when they
|
|
121
|
+
# declared none -- in which case `tan.model.perf_apply._resolve_hw_rev` falls
|
|
122
|
+
# back to the SKU preset's `default_hw_rev`, which is what the SDK itself does
|
|
123
|
+
# for an omitted key. It reaches ONLY the perf-point step: a bench measurement
|
|
124
|
+
# is pinned to the module revision it ran on, and nothing else in `check` is.
|
|
125
|
+
def check_model_backends(*, backends: list[str], sku: str, source: Path,
|
|
126
|
+
metadata_root: Path, exact: bool,
|
|
127
|
+
hw_rev: str | None = None,
|
|
128
|
+
board_doc: dict | None = None) -> list[BackendReport]:
|
|
129
|
+
"""One `BackendReport` per @backends entry for @source -- the ops are
|
|
130
|
+
walked ONCE (`extract_ops`, best-effort: `[]` for a non-.tflite source,
|
|
131
|
+
same contract every ONNX-ingesting backend already treats as
|
|
132
|
+
`undetermined` via `analyze_backend`'s own format gate) and reused across
|
|
133
|
+
every backend, mirroring `build_model`'s own single-read-many-adapters
|
|
134
|
+
shape.
|
|
135
|
+
|
|
136
|
+
A `.tflite` @source that extracts to `[]` is ambiguous on its own -- a
|
|
137
|
+
genuinely op-free/malformed model and a missing `tflite` reader (the
|
|
138
|
+
`model-io` extra) both look identical to `analyze_backend`. Checked here,
|
|
139
|
+
once, rather than inside `analyze_backend` (which stays format-agnostic
|
|
140
|
+
and knows nothing about `model-io`): when the reader is the actual cause,
|
|
141
|
+
the resulting `undetermined` report's note is swapped for one that names
|
|
142
|
+
the fix (`_reader_missing_report`), never left as the generic "nothing to
|
|
143
|
+
score" text that gives a `pip install tan-cli` customer -- exactly the
|
|
144
|
+
shape `ci.yml` itself installs, no `model-io` extra -- no hint at all
|
|
145
|
+
(MAJOR 2 review).
|
|
146
|
+
|
|
147
|
+
See the block comment above for the perf-point step's two properties: the
|
|
148
|
+
lazy digest behind `has_perf_points`, and what @hw_rev is.
|
|
149
|
+
|
|
150
|
+
@board_doc (tan-cli#1288) is `board.yaml` itself, or `None` -- threaded
|
|
151
|
+
down to `_exact_ethos_u`'s SRAM-fit check, which reads
|
|
152
|
+
`cores.<id>.inference.default_arena_kib` off it. `None` (the default) is
|
|
153
|
+
not a refusal: `resolve_arena_kib` falls back to the schema default."""
|
|
154
|
+
ops = extract_ops(source)
|
|
155
|
+
src_format = source.suffix.lstrip(".").lower()
|
|
156
|
+
reader_missing = src_format == "tflite" and not ops and not tflite_reader_available()
|
|
157
|
+
perf_published = has_perf_points(metadata_root)
|
|
158
|
+
resolved_hw_rev = (_resolve_hw_rev(sku, metadata_root, hw_rev)
|
|
159
|
+
if perf_published else None)
|
|
160
|
+
model_sha256: str | None = None
|
|
161
|
+
reports: list[BackendReport] = []
|
|
162
|
+
for backend in backends:
|
|
163
|
+
variant = (resolve_ethos_u_variant(sku, metadata_root=metadata_root)
|
|
164
|
+
if backend == "ethos_u" else None)
|
|
165
|
+
report = analyze_backend(backend=backend, src_format=src_format, ops=ops,
|
|
166
|
+
metadata_root=metadata_root, variant=variant)
|
|
167
|
+
if reader_missing and report.table is not None and not report.ops:
|
|
168
|
+
report = _reader_missing_report(report)
|
|
169
|
+
exact_target: TargetSpec | None = None
|
|
170
|
+
if exact:
|
|
171
|
+
report, exact_target = _apply_exact(report, backend, source, sku,
|
|
172
|
+
metadata_root, board_doc)
|
|
173
|
+
if perf_published:
|
|
174
|
+
if model_sha256 is None:
|
|
175
|
+
model_sha256 = _model_sha256(source)
|
|
176
|
+
# `target` is resolved here, once per backend, and handed to
|
|
177
|
+
# `apply_perf_point` rather than let it re-resolve internally --
|
|
178
|
+
# `tan.model.perf_apply` must never import `_headline_ethos_u_
|
|
179
|
+
# target` back out of this module (that would be the cycle this
|
|
180
|
+
# split exists to avoid), so the caller that already has this
|
|
181
|
+
# function is the one that calls it.
|
|
182
|
+
# Under `--exact` it is the target the reported compile came from
|
|
183
|
+
# (tan-cli#1497: not always the headline one), so a matched bench
|
|
184
|
+
# point re-bases the SAME target's figures.
|
|
185
|
+
target = ((exact_target or _headline_ethos_u_target(sku, metadata_root))
|
|
186
|
+
if backend == "ethos_u" else None)
|
|
187
|
+
report = apply_perf_point(report, backend=backend, sku=sku,
|
|
188
|
+
model_sha256=model_sha256,
|
|
189
|
+
hw_rev=resolved_hw_rev,
|
|
190
|
+
metadata_root=metadata_root,
|
|
191
|
+
target=target)
|
|
192
|
+
reports.append(report)
|
|
193
|
+
return reports
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _reader_missing_report(report: BackendReport) -> BackendReport:
|
|
197
|
+
"""Swap `_empty_ops_report`'s generic "nothing was extracted" note for one
|
|
198
|
+
that names the actual, fixable cause -- the `tflite` reader (the
|
|
199
|
+
`model-io` extra) is not installed on this host -- without touching
|
|
200
|
+
`npu_coverage`: this is still `undetermined`, never a verdict on the
|
|
201
|
+
model itself. Only called when a resolved table (`report.table is not
|
|
202
|
+
None`) proves the format/backend pairing was fine and ops really did come
|
|
203
|
+
back empty for a reader reason, never for `format-not-accepted` or
|
|
204
|
+
`no-table-for-backend` (both already carry their own honest note and
|
|
205
|
+
leave `report.table` `None`)."""
|
|
206
|
+
return replace(report, notes=[
|
|
207
|
+
"no operators could be extracted: the `tflite` reader is not "
|
|
208
|
+
"installed on this host (pip install tan-cli[model-io]); this is "
|
|
209
|
+
"not a statement about the model itself.",
|
|
210
|
+
])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _apply_exact(report: BackendReport, backend: str, source: Path, sku: str,
|
|
214
|
+
metadata_root: Path, board_doc: dict | None = None,
|
|
215
|
+
) -> tuple[BackendReport, TargetSpec | None]:
|
|
216
|
+
if backend != "ethos_u":
|
|
217
|
+
return _license_gated_exact_note(report, backend), None
|
|
218
|
+
return _exact_ethos_u(report, source, sku, metadata_root, board_doc)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _license_gated_exact_note(report: BackendReport, backend: str) -> BackendReport:
|
|
222
|
+
"""`drpai`/`deepx_dxm1` never attempt a real compile under `--exact` in
|
|
223
|
+
this release -- both toolchains are license-gated (`dxcom`, the DRP-AI
|
|
224
|
+
TVM checkout), so there is no un-gated path the way `vela` is for
|
|
225
|
+
ethos_u. Reported as a note, never a crash -- mirrors `model doctor`'s
|
|
226
|
+
own `_deepx_dxm1_status`/`_drpai_status` treatment of the same two
|
|
227
|
+
backends.
|
|
228
|
+
|
|
229
|
+
TWO SEPARATE NOTES, not one (tan-cli#791 round-2 review item 4): the HOST
|
|
230
|
+
fact ("--exact is not available ...: {tool} is license-gated.") and the
|
|
231
|
+
BASIS clause ("This stays the static screen.") used to be one string, so
|
|
232
|
+
`tan.model.perf_apply._perf_point_report`'s "keep every note starting
|
|
233
|
+
with `--exact`" rule kept the WHOLE thing -- including the basis clause
|
|
234
|
+
-- when a bench point for this exact identity later matched, landing
|
|
235
|
+
"this stays the static screen" alongside an envelope whose `basis` was
|
|
236
|
+
already `"bench"`. Splitting them means the filter's own prefix check
|
|
237
|
+
does the rest: the host note still starts with `--exact` and survives (it
|
|
238
|
+
is still true that `--exact` cannot compile this backend); the basis
|
|
239
|
+
clause does not start with `--exact` and is dropped automatically,
|
|
240
|
+
exactly the way every other basis-describing note already is."""
|
|
241
|
+
if report.ops and report.ops[0].reason == "format-not-accepted":
|
|
242
|
+
return report # wrong ingest format either way; nothing to caveat
|
|
243
|
+
tool = "dxcom" if backend == "deepx_dxm1" else "the DRP-AI TVM toolchain"
|
|
244
|
+
host_note = (f"--exact is not available for {backend} in this release: "
|
|
245
|
+
f"{tool} is license-gated.")
|
|
246
|
+
return replace(report, notes=[*report.notes, host_note,
|
|
247
|
+
"This stays the static screen."])
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
#: A note is one line in the JSON envelope and the text render; a failed
|
|
251
|
+
#: `vela` subprocess can raise `RuntimeError` wrapping its own raw stderr,
|
|
252
|
+
#: which can be a multi-hundred-character, multi-newline Python traceback
|
|
253
|
+
#: (measured: 750 characters, 9 newlines) that must never land verbatim in
|
|
254
|
+
#: either (MAJOR 2 review). This budget governs FOREIGN text -- whatever
|
|
255
|
+
#: arbitrary thing vela's stderr happened to contain.
|
|
256
|
+
_VELA_ERR_NOTE_BUDGET = 200
|
|
257
|
+
|
|
258
|
+
#: `VelaFootprintRefused` gets a wider one, because it is a different kind of
|
|
259
|
+
#: string: tan AUTHORS it, one line by contract, from a fixed template over a
|
|
260
|
+
#: handful of bounded fields (accel config, two op counts, vela's own memory
|
|
261
|
+
#: areas + resolved profile names). At 200 it was cut at "... Refusing to
|
|
262
|
+
#: report a zero…" -- measured inner length 197 -- so the customer-facing note
|
|
263
|
+
#: in the text report AND the JSON envelope carried the diagnosis with none of
|
|
264
|
+
#: the remediation, which is the whole reason the refusal was reworded
|
|
265
|
+
#: (tan-cli#789 review BLOCKER 2 / MINOR 5). The two numbers below are RE-RUN
|
|
266
|
+
#: against the live template every time it is reworded, never carried forward
|
|
267
|
+
#: with a delta applied: a real `ethos-u85-256` refusal is 623 characters
|
|
268
|
+
#: (measured by compiling `tests/fixtures/models/tiny_int8.tflite` at
|
|
269
|
+
#: `ethos-u85-256` with real `ethos-u-vela` 5.1.0, carrying e8.json's own
|
|
270
|
+
#: `vendor_config_filename` and its no-DRAM answer, then taking
|
|
271
|
+
#: `len(str(exc))`), and a deliberately maximal one (three memory areas at
|
|
272
|
+
#: five significant figures, the longest profile name vela emits, four-digit
|
|
273
|
+
#: op counts, both metadata clauses) is 691 --
|
|
274
|
+
#: `test_the_refusal_note_budget_covers_a_maximal_refusal` builds exactly that
|
|
275
|
+
#: and fails if a future message outgrows this. Both were 3 and 2 characters
|
|
276
|
+
#: over-stated when last edited, because the reword's delta was estimated
|
|
277
|
+
#: rather than the message re-measured (tan-cli#789 review MINOR 1); no gate
|
|
278
|
+
#: reads either figure, so an assumed one rots silently. Still bounded and still
|
|
279
|
+
#: word-boundary-truncated with a trailing marker, so the one-line guarantee
|
|
280
|
+
#: is unchanged; the 750-character, 9-newline traceback stays governed by
|
|
281
|
+
#: `_VELA_ERR_NOTE_BUDGET` above, not by this.
|
|
282
|
+
_VELA_REFUSAL_NOTE_BUDGET = 700
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
#: Python's own traceback banner. When @err's message wraps a raw traceback (a
|
|
286
|
+
#: failed `vela` subprocess re-raising its stderr verbatim), the FIRST line is
|
|
287
|
+
#: the only one with no diagnostic content in it -- measured on a real run the
|
|
288
|
+
#: note read "--exact compile with vela failed (vela failed for ethos-u85-256:
|
|
289
|
+
#: Traceback (most recent call last):); reporting the static screen instead.",
|
|
290
|
+
#: discarding the actual cause ("RuntimeError: Compilation failed: No networks
|
|
291
|
+
#: defined via GraphAPI") that sat one line further down.
|
|
292
|
+
_TRACEBACK_BANNER = "Traceback (most recent call last):"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _short_vela_error(err: Exception, *, budget: int = _VELA_ERR_NOTE_BUDGET) -> str:
|
|
296
|
+
"""ONE line of @err's message, cut to @budget characters on a word
|
|
297
|
+
boundary with a trailing `…` when it doesn't already fit -- mirrors
|
|
298
|
+
`tan.core.doctor_render._truncate_fix`'s word-boundary-plus-ellipsis
|
|
299
|
+
convention. Never a mid-word cut, never a bare truncation with no marker,
|
|
300
|
+
and never a multi-line traceback.
|
|
301
|
+
|
|
302
|
+
WHICH line: for a wrapped traceback, the LAST non-empty one -- the
|
|
303
|
+
exception line, the only one that says what actually went wrong -- kept
|
|
304
|
+
behind whatever @err's own first line said BEFORE the banner (the accel
|
|
305
|
+
config, in `VelaAdapter.compile`'s "vela failed for ethos-u85-256:"), so
|
|
306
|
+
the note names both the target and the cause. Otherwise the first line,
|
|
307
|
+
unchanged.
|
|
308
|
+
|
|
309
|
+
@budget is a parameter rather than the bare constant because the two
|
|
310
|
+
callers pass genuinely different kinds of string: arbitrary foreign vela
|
|
311
|
+
stderr (`_VELA_ERR_NOTE_BUDGET`) versus tan's own single-line
|
|
312
|
+
`VelaFootprintRefused` template (`_VELA_REFUSAL_NOTE_BUDGET`). Both are
|
|
313
|
+
bounded; only the number differs."""
|
|
314
|
+
text = str(err).strip()
|
|
315
|
+
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
|
|
316
|
+
if not lines:
|
|
317
|
+
return text
|
|
318
|
+
if _TRACEBACK_BANNER in text and len(lines) > 1:
|
|
319
|
+
prefix = lines[0].split(_TRACEBACK_BANNER, 1)[0].strip()
|
|
320
|
+
line = f"{prefix} {lines[-1]}".strip() if prefix else lines[-1]
|
|
321
|
+
else:
|
|
322
|
+
line = lines[0]
|
|
323
|
+
if len(line) <= budget:
|
|
324
|
+
return line
|
|
325
|
+
cut = line[:budget]
|
|
326
|
+
boundary = cut.rfind(" ")
|
|
327
|
+
if boundary > 0:
|
|
328
|
+
cut = cut[:boundary]
|
|
329
|
+
return f"{cut}…"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _footprint_refused_note(report: BackendReport, err: Exception) -> BackendReport:
|
|
333
|
+
"""`--exact` degrades to the static screen when tan refuses the footprint
|
|
334
|
+
of an otherwise CLEAN vela run -- and says exactly that.
|
|
335
|
+
|
|
336
|
+
NOT "compile with vela failed" (tan-cli#789 review NIT 8): vela exited 0,
|
|
337
|
+
wrote its output and printed a real placement summary. What failed was
|
|
338
|
+
tan's acceptance of the footprint it reported. Wording the two the same
|
|
339
|
+
would send a customer hunting a vela bug that isn't there.
|
|
340
|
+
|
|
341
|
+
The refusal text passes through at `_VELA_REFUSAL_NOTE_BUDGET`, so the
|
|
342
|
+
remediation clause at its tail survives into the text report and the JSON
|
|
343
|
+
envelope instead of being cut off mid-diagnosis.
|
|
344
|
+
|
|
345
|
+
TWO NOTES, not one (tan-cli#791 round-2 review item 4): `detail` is the
|
|
346
|
+
HOST/toolchain fact (vela's own refusal, still true of this run whatever
|
|
347
|
+
else happens); "Reporting the static screen instead." is a BASIS clause
|
|
348
|
+
that must not survive a re-base onto a matched bench point. Splitting
|
|
349
|
+
them lets `tan.model.perf_apply._perf_point_report`'s own "keep every
|
|
350
|
+
`--exact`-prefixed note" filter drop the second sentence for free -- it
|
|
351
|
+
does not start with `--exact` -- rather than keeping the whole string
|
|
352
|
+
verbatim next to a `basis: "bench"` envelope."""
|
|
353
|
+
detail = _short_vela_error(err, budget=_VELA_REFUSAL_NOTE_BUDGET)
|
|
354
|
+
return replace(report, notes=[*report.notes, f"--exact: {detail}",
|
|
355
|
+
"Reporting the static screen instead."])
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _exact_ethos_u(report: BackendReport, source: Path, sku: str,
|
|
359
|
+
metadata_root: Path, board_doc: dict | None = None,
|
|
360
|
+
) -> tuple[BackendReport, TargetSpec | None]:
|
|
361
|
+
"""Runs the real `vela` compile when it is on PATH; degrades cleanly --
|
|
362
|
+
and SAYS SO, in a note -- back to @report (the static screen) for every
|
|
363
|
+
other case: no vela, no resolvable accelerator config, a vela failure, or
|
|
364
|
+
a footprint tan refused. Never raises. The compile succeeding is handed to
|
|
365
|
+
`_report_from_vela_compile`, which is what actually decides `npu_coverage`
|
|
366
|
+
-- NOT this function, and NOT vela's exit code (0 on a full CPU fallback
|
|
367
|
+
by design).
|
|
368
|
+
|
|
369
|
+
The refusal gets its OWN except-branch ahead of the general one
|
|
370
|
+
(`_footprint_refused_note`): "compile with vela failed" is the wrong
|
|
371
|
+
sentence for a run that exited 0 and printed a real placement summary, and
|
|
372
|
+
the refusal's remediation tail needs the wider note budget to survive.
|
|
373
|
+
|
|
374
|
+
Gated on `VelaAdapter().accepts(...)` directly, NOT on
|
|
375
|
+
`report.ops[0].reason == "format-not-accepted"`: `extract_ops` (tensorio)
|
|
376
|
+
only ever extracts operators from a `.tflite` source, so `report.ops` is
|
|
377
|
+
`[]` for every OTHER format too -- meaning the reason-based check was
|
|
378
|
+
dead code for exactly the case it existed to guard (ethos_u only ingests
|
|
379
|
+
"tflite", so it only ever GETS "format-not-accepted" for a non-.tflite
|
|
380
|
+
source, which is precisely when `ops` is always empty). Left ungated,
|
|
381
|
+
`VelaAdapter().compile()` was invoked on e.g. a `.onnx` source under
|
|
382
|
+
`--exact` on an Ethos-U SKU, and a `vela` failure on unparseable input
|
|
383
|
+
can raise with its own raw, multi-line traceback as the message (MAJOR 2
|
|
384
|
+
review: measured a 750-character, 9-newline traceback landing in
|
|
385
|
+
`notes[1]` of the JSON envelope this way)."""
|
|
386
|
+
if not VelaAdapter().accepts(source.suffix.lstrip(".").lower()):
|
|
387
|
+
return report, None
|
|
388
|
+
if shutil.which("vela") is None:
|
|
389
|
+
# TWO notes, not one (tan-cli#791 round-2 review item 4): "vela is
|
|
390
|
+
# not on PATH" is a HOST fact, still true of this run even when a
|
|
391
|
+
# bench point later matches for this exact identity; "reporting the
|
|
392
|
+
# static screen instead" is a BASIS clause that becomes false the
|
|
393
|
+
# moment it does. Kept as one string, the whole thing used to survive
|
|
394
|
+
# `tan.model.perf_apply._perf_point_report`'s "keep every
|
|
395
|
+
# `--exact`-prefixed note" filter and land next to a `basis: "bench"`
|
|
396
|
+
# envelope. Split, only the host sentence starts with `--exact`; the
|
|
397
|
+
# basis sentence is dropped by that same filter automatically.
|
|
398
|
+
return replace(report, notes=[
|
|
399
|
+
*report.notes,
|
|
400
|
+
"--exact was requested, but vela is not on PATH (pip install "
|
|
401
|
+
"tan-cli[model-compile]).",
|
|
402
|
+
"Reporting the static screen instead.",
|
|
403
|
+
]), None
|
|
404
|
+
headline = _headline_ethos_u_target(sku, metadata_root)
|
|
405
|
+
if headline is None:
|
|
406
|
+
# NOT split like the sites above (tan-cli#791 round-2 review item 4):
|
|
407
|
+
# `check_model_backends` resolves this SAME `_headline_ethos_u_target`
|
|
408
|
+
# call, with the SAME sku/metadata_root, for the tier-2 step -- a pure
|
|
409
|
+
# function of metadata, so if it is `None` HERE it is `None` there
|
|
410
|
+
# too, and `_resolve_perf_point` refuses outright on `backend ==
|
|
411
|
+
# "ethos_u" and target is None` before it ever looks at a point. This
|
|
412
|
+
# note can therefore never ride alongside a `basis: "bench"` report,
|
|
413
|
+
# so its basis clause is never false.
|
|
414
|
+
note = ("--exact could not resolve an Ethos-U accelerator config for "
|
|
415
|
+
"this SKU; reporting the static screen instead.")
|
|
416
|
+
return replace(report, notes=[*report.notes, note]), None
|
|
417
|
+
# tan-cli#1497: `tan model build` compiles EVERY ethos_u target and refuses
|
|
418
|
+
# the model only when none of them fits, so `--exact` must too -- a
|
|
419
|
+
# headline-only check exited 2 while build shipped the targets that fit.
|
|
420
|
+
# The headline target goes first (it keeps deciding a lone-target SKU's
|
|
421
|
+
# report exactly as before); the first target that compiles AND fits wins;
|
|
422
|
+
# when none does, the headline result stands, so a model no target can
|
|
423
|
+
# run still reports its `model.sram-no-fit`.
|
|
424
|
+
results: list[tuple[TargetSpec, BackendReport, Blob | None]] = []
|
|
425
|
+
for cand in _exact_ethos_u_targets(sku, metadata_root, headline):
|
|
426
|
+
res, blob = _exact_one_target(report, source, cand, sku, metadata_root,
|
|
427
|
+
board_doc)
|
|
428
|
+
results.append((cand, res, blob))
|
|
429
|
+
chosen_target, chosen, _ = results[0]
|
|
430
|
+
for cand, res, blob in results:
|
|
431
|
+
# Same skip `build` applies: a compile that places nothing on the
|
|
432
|
+
# accelerator is not an accelerator target.
|
|
433
|
+
if (blob is not None and res.basis == "compiled"
|
|
434
|
+
and not (res.sram_fit and res.sram_fit.no_fit)
|
|
435
|
+
and not _placed_nothing_on_accelerator("ethos_u", blob)):
|
|
436
|
+
chosen_target, chosen = cand, res
|
|
437
|
+
break
|
|
438
|
+
else:
|
|
439
|
+
return chosen, chosen_target
|
|
440
|
+
if chosen_target == headline:
|
|
441
|
+
return chosen, chosen_target
|
|
442
|
+
extra: list[str] = []
|
|
443
|
+
skipped = [c.accel_config for c, r, _b in results
|
|
444
|
+
if r.sram_fit is not None and r.sram_fit.no_fit]
|
|
445
|
+
if skipped:
|
|
446
|
+
extra.append(
|
|
447
|
+
f"--exact: ethos_u target(s) {', '.join(skipped)} do not fit SRAM; "
|
|
448
|
+
f"`tan model build` skips them and ships {chosen_target.accel_config}.")
|
|
449
|
+
nothing = [c.accel_config for c, _r, b in results
|
|
450
|
+
if b is not None and _placed_nothing_on_accelerator("ethos_u", b)]
|
|
451
|
+
if nothing:
|
|
452
|
+
extra.append(
|
|
453
|
+
f"--exact: ethos_u target(s) {', '.join(nothing)} place no operator on "
|
|
454
|
+
f"the NPU; `tan model build` skips them.")
|
|
455
|
+
# A failed or refused earlier target must not vanish when a later one wins:
|
|
456
|
+
# carry its host-fact notes (the "Reporting the static screen instead"
|
|
457
|
+
# basis clause is false for the winner, so only `--exact` notes survive).
|
|
458
|
+
for cand, res, blob in results:
|
|
459
|
+
if blob is None and cand != chosen_target:
|
|
460
|
+
extra += [f"{n} [target {cand.accel_config}]"
|
|
461
|
+
for n in res.notes[len(report.notes):]
|
|
462
|
+
if n.startswith("--exact")]
|
|
463
|
+
if extra:
|
|
464
|
+
chosen = replace(chosen, notes=[*chosen.notes, *extra])
|
|
465
|
+
return chosen, chosen_target
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _exact_ethos_u_targets(sku: str, metadata_root: Path,
|
|
469
|
+
headline: TargetSpec) -> list[TargetSpec]:
|
|
470
|
+
"""Every ethos_u target `tan model build` compiles for @sku, the headline
|
|
471
|
+
one first (tan-cli#1497)."""
|
|
472
|
+
rest = [s for s in resolve_targets(sku, metadata_root=metadata_root)
|
|
473
|
+
if s.backend == "ethos_u" and s != headline]
|
|
474
|
+
return [headline, *rest]
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _screen_for_target(report: BackendReport, source: Path, target: TargetSpec,
|
|
478
|
+
metadata_root: Path) -> BackendReport:
|
|
479
|
+
"""tan-cli#1519: the static screen's `variant`/`table`/per-op verdicts come
|
|
480
|
+
from the SoM preset's headline variant; a target of another variant (u55 on
|
|
481
|
+
a u85-headline SKU) must be reported against ITS OWN table, not the
|
|
482
|
+
headline's. Same-variant targets, and any failure to re-screen, keep
|
|
483
|
+
@report unchanged."""
|
|
484
|
+
variant = target.accel_config.split("-")[1] if target.accel_config.count("-") >= 2 else None
|
|
485
|
+
if not variant or report.variant is None or variant == report.variant:
|
|
486
|
+
return report
|
|
487
|
+
try:
|
|
488
|
+
rescreened = analyze_backend(
|
|
489
|
+
backend="ethos_u", src_format=source.suffix.lstrip(".").lower(),
|
|
490
|
+
ops=extract_ops(source), metadata_root=metadata_root, variant=variant)
|
|
491
|
+
except Exception as err: # noqa: BLE001 -- a failed re-screen keeps the headline screen
|
|
492
|
+
# ...but says so: u85 variant/table next to u55 compile figures is the
|
|
493
|
+
# #1519 mislabel, and must not be silent.
|
|
494
|
+
return replace(report, notes=[
|
|
495
|
+
*report.notes,
|
|
496
|
+
f"--exact: could not re-screen against {variant}'s table "
|
|
497
|
+
f"({str(err)[:120]}); variant/table below are the headline's."])
|
|
498
|
+
return replace(
|
|
499
|
+
report, variant=rescreened.variant, table=rescreened.table, ops=rescreened.ops,
|
|
500
|
+
npu_coverage=rescreened.npu_coverage,
|
|
501
|
+
compute_on_npu_pct_max=rescreened.compute_on_npu_pct_max,
|
|
502
|
+
uncosted_cpu_op_count=rescreened.uncosted_cpu_op_count)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _exact_one_target(report: BackendReport, source: Path, target: TargetSpec,
|
|
506
|
+
sku: str, metadata_root: Path,
|
|
507
|
+
board_doc: dict | None,
|
|
508
|
+
) -> tuple[BackendReport, Blob | None]:
|
|
509
|
+
"""Re-screen @report against @target's own variant, then compile it. A
|
|
510
|
+
successful compile rebuilds `notes` from scratch, so a re-screen-failure
|
|
511
|
+
note (host fact, starts with `--exact`) is re-added on that path."""
|
|
512
|
+
screened = _screen_for_target(report, source, target, metadata_root)
|
|
513
|
+
kept = screened.notes[len(report.notes):]
|
|
514
|
+
out, blob = _exact_compile_target(screened, source, target, sku, metadata_root, board_doc)
|
|
515
|
+
if kept and blob is not None:
|
|
516
|
+
out = replace(out, notes=[*out.notes, *kept])
|
|
517
|
+
return out, blob
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _exact_compile_target(report: BackendReport, source: Path, target: TargetSpec,
|
|
521
|
+
sku: str, metadata_root: Path,
|
|
522
|
+
board_doc: dict | None,
|
|
523
|
+
) -> tuple[BackendReport, Blob | None]:
|
|
524
|
+
"""The real vela compile of ONE target, degrading to a note on @report on
|
|
525
|
+
any failure (blob is then `None`). Never raises."""
|
|
526
|
+
accel_config = target.accel_config
|
|
527
|
+
try:
|
|
528
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
529
|
+
# Accel-config, memory profile AND the part's diagnostic facts all
|
|
530
|
+
# come off the SAME @target (one of `tan model build`'s ethos_u targets), so
|
|
531
|
+
# `--exact` reports what `tan model build` would actually compile
|
|
532
|
+
# for this SKU -- a screen run against a different memory model than
|
|
533
|
+
# the build uses would be a figure the customer cannot act on, and a
|
|
534
|
+
# refusal here has to read exactly as the build's would.
|
|
535
|
+
blob = VelaAdapter().compile(
|
|
536
|
+
source, accel_config=accel_config, out_dir=Path(tmp),
|
|
537
|
+
vela_memory_mode=target.vela_memory_mode,
|
|
538
|
+
vela_system_config=target.vela_system_config,
|
|
539
|
+
vela_vendor_system_config=target.vela_vendor_system_config,
|
|
540
|
+
vela_vendor_config_filename=target.vela_vendor_config_filename,
|
|
541
|
+
soc_declares_dram=target.soc_declares_dram)
|
|
542
|
+
except VelaFootprintRefused as err:
|
|
543
|
+
return _footprint_refused_note(report, err), None
|
|
544
|
+
except Exception as err: # noqa: BLE001 -- a failed exact compile degrades, it never crashes
|
|
545
|
+
# TWO notes, not one (tan-cli#791 round-2 review item 4) -- same split
|
|
546
|
+
# as the "vela is not on PATH" branch above, for the same reason: the
|
|
547
|
+
# HOST fact (vela really did fail, and why) must survive a re-base
|
|
548
|
+
# onto a matched bench point; the BASIS clause ("reporting the static
|
|
549
|
+
# screen instead") must not.
|
|
550
|
+
return replace(report, notes=[
|
|
551
|
+
*report.notes,
|
|
552
|
+
f"--exact compile with vela failed ({_short_vela_error(err)}).",
|
|
553
|
+
"Reporting the static screen instead.",
|
|
554
|
+
]), None
|
|
555
|
+
# tan-cli#1288: the SRAM0 residency + arena fit check, off the SAME
|
|
556
|
+
# resolved target/blob a real compile just produced -- `target.paired_core`
|
|
557
|
+
# (which `cores.<id>.inference.default_arena_kib` applies) and
|
|
558
|
+
# `target.vela_memory_mode` (whether the check applies at all) both come
|
|
559
|
+
# from @target (resolved once by the caller), never re-resolved.
|
|
560
|
+
sram_fit = evaluate_sram_fit(
|
|
561
|
+
memory_mode=target.vela_memory_mode, req_sram_kib=blob.req_sram_kib,
|
|
562
|
+
blob_len_bytes=len(blob.payload), board_doc=board_doc,
|
|
563
|
+
paired_core=target.paired_core, sku=sku, metadata_root=metadata_root,
|
|
564
|
+
)
|
|
565
|
+
return _report_from_vela_compile(report, blob, accel_config, sram_fit=sram_fit), blob
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _vela_placement_note(blob: Blob, accel_config: str, total: int, pct: float,
|
|
569
|
+
sizing: str) -> str:
|
|
570
|
+
"""The one-line placement summary a `basis: "compiled"` report leads with.
|
|
571
|
+
|
|
572
|
+
`blob.compiler_version` is ALREADY the full string to show -- "vela"
|
|
573
|
+
(degraded) or "vela X.Y.Z" (real); wrapping it in a second "vela (...)"
|
|
574
|
+
would read "vela (vela)" on the degraded path."""
|
|
575
|
+
return (f"{blob.compiler_version} compiled for {accel_config}: "
|
|
576
|
+
f"{blob.npu_op_count}/{total} operators placed on the NPU "
|
|
577
|
+
f"({pct:.0f}%); {sizing}.")
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# `arena_bytes`/`req_sram_kib` are carried as REPORT FIELDS as well as inside
|
|
581
|
+
# `sizing`'s prose below: `_perf_disagreements` compares a bench point against
|
|
582
|
+
# a compiled report figure-by-figure, and parsing tan's own sentence back to do
|
|
583
|
+
# that would be a second, silently-drifting spelling of the same two numbers.
|
|
584
|
+
def _report_from_vela_compile(report: BackendReport, blob: Blob, accel_config: str,
|
|
585
|
+
sram_fit: SramFit | None = None) -> BackendReport:
|
|
586
|
+
"""Turn a REAL, successful vela compile into a `BackendReport` driven by
|
|
587
|
+
what vela actually placed on the NPU -- never by the fact that compile()
|
|
588
|
+
didn't raise. **Vela exits 0 on a full CPU fallback by design** (measured:
|
|
589
|
+
a float32 FULLY_CONNECTED model compiled for `ethos-u85-256` prints "NPU
|
|
590
|
+
operators = 0 (0.0%)" and still exits 0), so "it didn't crash" is not
|
|
591
|
+
proof of NPU placement -- that overclaim is exactly what this function
|
|
592
|
+
replaces (the BLOCKER this fix addresses).
|
|
593
|
+
|
|
594
|
+
Reads `blob.cpu_op_count`/`npu_op_count` (vela's own placement verdict):
|
|
595
|
+
100% NPU (no CPU ops, at least one op ran) is the only case that reports
|
|
596
|
+
`"fits"`, the only `npu_coverage` this module ever emits at `basis:
|
|
597
|
+
"compiled"` -- `ops` stays `[]`. Partial or 0% NPU reports the REAL split
|
|
598
|
+
(`npu_placement_pct_real`) and KEEPS @report's own per-op verdicts
|
|
599
|
+
(`ops=report.ops`) rather than discarding them -- a 0%-NPU compile must
|
|
600
|
+
never read as a bare, contextless success. An unparseable placement
|
|
601
|
+
summary degrades to `_vela_placement_unreadable`, same as every other
|
|
602
|
+
`_exact_ethos_u` failure path.
|
|
603
|
+
|
|
604
|
+
`pct` -- vela's aggregate CPU/NPU op *count* split -- goes into
|
|
605
|
+
`npu_placement_pct_real`, NEVER into `compute_on_npu_pct_max` (left
|
|
606
|
+
`None` here): the latter's contract is a MAC-weighted figure
|
|
607
|
+
(`analyze.BackendReport`'s own field comment), and vela's placement
|
|
608
|
+
summary carries no per-op MAC data to weight `pct` by -- reusing that
|
|
609
|
+
field for an op-count ratio was the unit mismatch MAJOR 4 review flagged.
|
|
610
|
+
|
|
611
|
+
`blob.caveats` follow the placement note verbatim and are never dropped
|
|
612
|
+
-- today, vela's own "may be invalid or non-optimal" verdict on the
|
|
613
|
+
BUILT-IN default profile it fell back to. A `basis: "compiled"` report
|
|
614
|
+
off a default-profile compile must say so; the figures above it describe
|
|
615
|
+
vela's default memory model, not the module's (see
|
|
616
|
+
`tan.model.adapters.ethos_u`'s module docstring)."""
|
|
617
|
+
sizing = f"arena {blob.arena_bytes} bytes, SRAM {blob.req_sram_kib} KiB"
|
|
618
|
+
outcome = _placement_outcome(report, blob.npu_op_count, blob.cpu_op_count)
|
|
619
|
+
if outcome is None:
|
|
620
|
+
return _vela_placement_unreadable(report, blob, accel_config, sizing)
|
|
621
|
+
total = blob.npu_op_count + blob.cpu_op_count
|
|
622
|
+
pct = 100.0 * blob.npu_op_count / total
|
|
623
|
+
coverage, ops, uncosted = outcome
|
|
624
|
+
note = _vela_placement_note(blob, accel_config, total, pct, sizing)
|
|
625
|
+
return BackendReport(
|
|
626
|
+
backend="ethos_u", variant=report.variant, table=report.table,
|
|
627
|
+
npu_coverage=coverage, compute_on_npu_pct_max=None,
|
|
628
|
+
npu_placement_pct_real=pct, uncosted_cpu_op_count=uncosted, ops=ops,
|
|
629
|
+
basis="compiled", confidence="certain",
|
|
630
|
+
arena_bytes=blob.arena_bytes, req_sram_kib=blob.req_sram_kib,
|
|
631
|
+
notes=[note, *blob.caveats],
|
|
632
|
+
sram_fit=sram_fit,
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _vela_placement_unreadable(report: BackendReport, blob: Blob, accel_config: str,
|
|
637
|
+
sizing: str) -> BackendReport:
|
|
638
|
+
"""vela ran and produced output, but `_parse_vela_placement` found no
|
|
639
|
+
"CPU/NPU operators = N" lines in it (an unexpected output shape, e.g. a
|
|
640
|
+
future vela version that changes this text) -- degrade to @report (the
|
|
641
|
+
static screen) exactly like every other `_exact_ethos_u` failure
|
|
642
|
+
path. This function cannot verify a placement it can't read, so it does
|
|
643
|
+
not claim one -- never "0 CPU ops, so it must be all-NPU".
|
|
644
|
+
|
|
645
|
+
TWO notes, not one (tan-cli#791 round-2 review item 4) -- the same split
|
|
646
|
+
as every other degrade note in this module: the HOST fact (vela compiled,
|
|
647
|
+
but its placement summary could not be read) survives a re-base onto a
|
|
648
|
+
matched bench point; the BASIS clause ("reporting the static screen
|
|
649
|
+
instead") does not, because it stops being true the moment one exists."""
|
|
650
|
+
note = (f"--exact compiled with {blob.compiler_version} for {accel_config} "
|
|
651
|
+
f"({sizing}), but its operator-placement summary could not be read, "
|
|
652
|
+
f"so NPU placement cannot be verified.")
|
|
653
|
+
return replace(report, notes=[*report.notes, note,
|
|
654
|
+
"Reporting the static screen instead.", *blob.caveats])
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _headline_ethos_u_target(sku: str, metadata_root: Path) -> TargetSpec | None:
|
|
658
|
+
"""The target `--exact` hands `vela`: `resolve_ethos_u_variant`'s own
|
|
659
|
+
PRIMARY variant, at the highest `mac_per_cycle` config resolved for it --
|
|
660
|
+
an arbitrary but deterministic tiebreak for a SoM shipping more than one
|
|
661
|
+
NPU at the same variant (E1M-AEN701's `alif:ensemble:e7` lists a
|
|
662
|
+
high-perf U55 at 256 MAC/cycle alongside a high-efficiency U55 at 128,
|
|
663
|
+
`metadata/socs/alif/ensemble/e7.json`'s own `npus[]`). There is no
|
|
664
|
+
per-SKU "preferred accel_config" field in the metadata this could read
|
|
665
|
+
instead of guessing; None when the variant itself doesn't resolve
|
|
666
|
+
(drpai/deepx_dxm1 SoMs) or no target matches it.
|
|
667
|
+
|
|
668
|
+
Returns the whole `TargetSpec` rather than just its `accel_config` so the
|
|
669
|
+
caller can hand the adapter this part's memory profile and its diagnostic
|
|
670
|
+
facts alongside it (tan-cli#789 review (g)): all of them come off the SAME
|
|
671
|
+
resolved target, so `--exact` can never describe one target's accelerator
|
|
672
|
+
with another's memory model or another part's vendor `.ini`."""
|
|
673
|
+
variant = resolve_ethos_u_variant(sku, metadata_root=metadata_root)
|
|
674
|
+
if variant is None:
|
|
675
|
+
return None
|
|
676
|
+
prefix = f"ethos-{variant}-"
|
|
677
|
+
candidates = [s for s in resolve_targets(sku, metadata_root=metadata_root)
|
|
678
|
+
if s.backend == "ethos_u" and s.accel_config.startswith(prefix)]
|
|
679
|
+
if not candidates:
|
|
680
|
+
return None
|
|
681
|
+
return max(candidates, key=lambda s: int(s.accel_config.rsplit("-", 1)[-1]))
|
|
682
|
+
|