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/analyze.py
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Static NPU-eligibility screen for a packaged model (ADR-0028 amendment).
|
|
3
|
+
|
|
4
|
+
Answers, offline and with no NPU toolchain installed, "how much of this model
|
|
5
|
+
can target the NPU on this SoM, and what definitely cannot?" -- with claims
|
|
6
|
+
the evidence actually supports. Supersedes the retired `fits | cpu-fallback |
|
|
7
|
+
no-fit` vocabulary: no backend can deliver `fits` statically (Vela attaches
|
|
8
|
+
Generic constraints -- quantization, per-axis quant, dtype, zero-point, shape
|
|
9
|
+
-- to every operator and Specific ones to 30 of 70; DRP-AI gates acceptance on
|
|
10
|
+
enumerated kernel x stride x padding x dilation x groups, so the same operator
|
|
11
|
+
name is accepted or rejected on tensor shape alone). So:
|
|
12
|
+
|
|
13
|
+
* **Negatives are sound.** An operator absent from the resolved table is
|
|
14
|
+
certainly CPU (`cpu-certain`).
|
|
15
|
+
* **Positives are capped at "eligible", never "will run".** An operator
|
|
16
|
+
present in the table is `npu-eligible` -- a static screen cannot see the
|
|
17
|
+
shape/quantization decision a real compile makes.
|
|
18
|
+
* **A missing source-format match, or a missing support table, is
|
|
19
|
+
`undetermined`, never `cpu-only`.** Both read to a customer as "won't
|
|
20
|
+
run", which is false: every backend degrades to silent CPU fallback
|
|
21
|
+
rather than refusing. `deepx_dxm1` ships no table by decision -- DEEPX is
|
|
22
|
+
the headline feature of V2M, so a fabricated negative there is the worst
|
|
23
|
+
outcome this module exists to prevent.
|
|
24
|
+
|
|
25
|
+
The word "fits" is reserved for `basis: compiled` or `basis: bench`; this
|
|
26
|
+
module's `basis` is always `"static-screen"` and must never emit it.
|
|
27
|
+
|
|
28
|
+
Pure engine: no click/typer, no envelope construction, no file writes. Every
|
|
29
|
+
public function takes `metadata_root` explicitly rather than resolving an
|
|
30
|
+
ambient SDK checkout -- `tan.model` stays self-contained
|
|
31
|
+
(`tests/model/test_model_package_imports.py`)."""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
from collections.abc import Sequence
|
|
37
|
+
from dataclasses import dataclass, field, replace
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
from tan.core.shapes import matches_glob_suffix
|
|
41
|
+
|
|
42
|
+
from .adapters import CompilerAdapter
|
|
43
|
+
from .adapters.cpu import CpuAdapter
|
|
44
|
+
from .adapters.deepx import DeepxAdapter
|
|
45
|
+
from .adapters.drpai import DrpaiAdapter
|
|
46
|
+
from .adapters.ethos_u import VelaAdapter
|
|
47
|
+
from .adapters.executorch import ExecutorchAdapter
|
|
48
|
+
from .npu_ops_validate import TableValidator
|
|
49
|
+
from .sram_fit import SramFit
|
|
50
|
+
from .tensorio import OpDesc
|
|
51
|
+
|
|
52
|
+
# One adapter instance per source format a backend can ingest -- mirrors
|
|
53
|
+
# `build.py`'s own `_ADAPTERS` registry (deliberately a second, independent
|
|
54
|
+
# instantiation: these are stateless, side-effect-free objects, and importing
|
|
55
|
+
# `build.py`'s registry directly would reach into another module's private
|
|
56
|
+
# (`_`-prefixed) name for no benefit). A `backend` string maps to >= 1
|
|
57
|
+
# adapter -- e.g. "cpu" carries both `CpuAdapter` (.tflite) and
|
|
58
|
+
# `ExecutorchAdapter` (.pte) -- so the format gate below asks "does ANY
|
|
59
|
+
# adapter registered for this backend accept this format", the same question
|
|
60
|
+
# `build_model()` asks per model.
|
|
61
|
+
_ADAPTERS: list[CompilerAdapter] = [
|
|
62
|
+
CpuAdapter(), VelaAdapter(), DrpaiAdapter(), DeepxAdapter(), ExecutorchAdapter(),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _adapters_by_backend() -> dict[str, list[CompilerAdapter]]:
|
|
67
|
+
by_backend: dict[str, list[CompilerAdapter]] = {}
|
|
68
|
+
for a in _ADAPTERS:
|
|
69
|
+
by_backend.setdefault(a.backend, []).append(a)
|
|
70
|
+
return by_backend
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_ADAPTERS_BY_BACKEND = _adapters_by_backend()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _known_backend(backend: str) -> bool:
|
|
77
|
+
return backend in _ADAPTERS_BY_BACKEND
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _accepts(backend: str, src_format: str) -> bool:
|
|
81
|
+
return any(a.accepts(src_format) for a in _ADAPTERS_BY_BACKEND.get(backend, []))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class OpVerdict:
|
|
86
|
+
"""One operator's static verdict against a resolved support table."""
|
|
87
|
+
|
|
88
|
+
op: str # as spelled in the model's own vocabulary
|
|
89
|
+
status: str # "npu-eligible" | "cpu-certain" | "unknown"
|
|
90
|
+
reason: str # "op-not-in-table" | "constraint-unchecked"
|
|
91
|
+
# | "no-table-for-backend" | "format-not-accepted"
|
|
92
|
+
macs: int = 0 # 0 when not computable
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
#: The `npu_coverage` word a report states when it has no placement verdict of
|
|
96
|
+
#: its own to state. It means TWO different things depending on `basis` -- at
|
|
97
|
+
#: `"static-screen"`, "nothing was screened at all" (`_format_not_accepted_
|
|
98
|
+
#: report`/`_no_table_report`/`_empty_ops_report` below); at `"bench"`,
|
|
99
|
+
#: "a real measurement rebased this report but carried no placement, so the
|
|
100
|
+
#: coverage word is WITHHELD rather than borrowed from the static screen
|
|
101
|
+
#: underneath" (`tan.model.perf_apply._perf_point_report`). Named rather than
|
|
102
|
+
#: spelled inline at those four sites so the withholding rule and the
|
|
103
|
+
#: `"bench"` entry of `LEGITIMATE_COVERAGE_BY_BASIS` below cannot drift apart:
|
|
104
|
+
#: they are the same decision written once.
|
|
105
|
+
COVERAGE_WITHHELD = "undetermined"
|
|
106
|
+
|
|
107
|
+
#: `npu_coverage` at `basis: "compiled"` -- exactly what `tan.model.perf.
|
|
108
|
+
#: coverage_from_placement` can return off a REAL per-operator placement, the
|
|
109
|
+
#: one function in tan allowed to return `"fits"`. `None` from that function
|
|
110
|
+
#: (an unreadable placement summary) is not a fourth word: it degrades to the
|
|
111
|
+
#: static screen with its basis unchanged (`tan.model.check.
|
|
112
|
+
#: _vela_placement_unreadable`), so it contributes no `"compiled"` pair.
|
|
113
|
+
_COVERAGE_AT_COMPILED = frozenset({"fits", "partial", "cpu-only"})
|
|
114
|
+
|
|
115
|
+
#: THE ENUMERATION: `basis` -> every `npu_coverage` that is legitimate at it.
|
|
116
|
+
#:
|
|
117
|
+
#: `BackendReport.__post_init__` enforces this on EVERY construction, so an
|
|
118
|
+
#: illegitimate pair cannot reach an envelope, and
|
|
119
|
+
#: `tests/gates/test_model_check_doc_coverage_table.py` holds
|
|
120
|
+
#: `docs/model-check-static-screen.md`'s own `(npuCoverage, basis)` table to
|
|
121
|
+
#: it in both directions (tan-cli#1135 -- that page went false at five lines
|
|
122
|
+
#: on tan-cli#1115 with nothing to catch it).
|
|
123
|
+
#:
|
|
124
|
+
#: Each entry is derived from the code path that produces it, not restated
|
|
125
|
+
#: from prose:
|
|
126
|
+
#:
|
|
127
|
+
#: `"static-screen"` `_coverage_label` over a non-empty verdict list --
|
|
128
|
+
#: `"full-eligible"` / `"partial"` / `"cpu-only"` -- plus
|
|
129
|
+
#: `COVERAGE_WITHHELD` from the three no-verdict reports.
|
|
130
|
+
#: This module never emits any other basis.
|
|
131
|
+
#: `"compiled"` `_COVERAGE_AT_COMPILED`, above.
|
|
132
|
+
#: `"bench"` `_perf_point_report` CARRIES a `"compiled"` report's
|
|
133
|
+
#: own already-measured verdict, or withholds. So it is
|
|
134
|
+
#: the compiled set plus `COVERAGE_WITHHELD` BY
|
|
135
|
+
#: CONSTRUCTION here, not by a second hand-written list:
|
|
136
|
+
#: a new word reachable at `"compiled"` is reachable at
|
|
137
|
+
#: `"bench"` for free, exactly as the carry branch says.
|
|
138
|
+
LEGITIMATE_COVERAGE_BY_BASIS: dict[str, frozenset[str]] = {
|
|
139
|
+
"static-screen": frozenset({"full-eligible", "partial", "cpu-only", COVERAGE_WITHHELD}),
|
|
140
|
+
"compiled": _COVERAGE_AT_COMPILED,
|
|
141
|
+
"bench": _COVERAGE_AT_COMPILED | {COVERAGE_WITHHELD},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class BackendReport:
|
|
147
|
+
"""One backend's static-screen partition report for a model."""
|
|
148
|
+
|
|
149
|
+
backend: str # cpu | ethos_u | drpai | deepx_dxm1
|
|
150
|
+
variant: str | None # u85 | u55 | None
|
|
151
|
+
table: str | None # the table file that answered, or None
|
|
152
|
+
# WHICH words are legitimate here is NOT restated in this comment -- read
|
|
153
|
+
# `LEGITIMATE_COVERAGE_BY_BASIS` above, keyed by this report's own `basis`.
|
|
154
|
+
# It is the source of truth, `__post_init__` enforces it on construction,
|
|
155
|
+
# and a prose second copy of that enumeration in this exact spot is what
|
|
156
|
+
# drifted and cost tan-cli#1135. What stays here is only WHY the vocabulary
|
|
157
|
+
# is basis-dependent, which no dict can say: `tan.model.perf.coverage_from_
|
|
158
|
+
# placement` is the one function in tan allowed to return "fits", off a
|
|
159
|
+
# real vela compile's own placement summary, so only `basis: "compiled"`
|
|
160
|
+
# ever DERIVES a verdict. A `basis: "bench"` report derives nothing --
|
|
161
|
+
# alp-sdk's real model-perf schema carries no bench-measured placement
|
|
162
|
+
# split (tan-cli#1115) -- so `tan.model.perf_apply._perf_point_report`
|
|
163
|
+
# CARRIES this field from the report it is rebasing only when that
|
|
164
|
+
# report's basis is already "compiled", and otherwise WITHHOLDS it to
|
|
165
|
+
# `COVERAGE_WITHHELD` rather than promoting the static screen's name-level
|
|
166
|
+
# estimate to `confidence: "certain"`. analyze_backend() itself never
|
|
167
|
+
# emits "fits" -- `basis` stays "static-screen" in this module always.
|
|
168
|
+
npu_coverage: str
|
|
169
|
+
# MAC-weighted UPPER bound, 0-100 -- ONLY at `basis: "static-screen"`
|
|
170
|
+
# (this module's own `_score_ops`/`eligible_macs / total_macs`). Always
|
|
171
|
+
# `None` at `basis: "compiled"` (tan.model.check's `--exact` path):
|
|
172
|
+
# vela's own placement summary is an aggregate CPU/NPU *op count*, not a
|
|
173
|
+
# per-op MAC breakdown, so there is no real MAC-weighted figure to put
|
|
174
|
+
# here for a compile -- `npu_placement_pct_real` below carries the real
|
|
175
|
+
# (but op-count, not MAC-weighted) placement instead (MAJOR 4 review: a
|
|
176
|
+
# `basis: "compiled"` report used to overload THIS field with that
|
|
177
|
+
# op-count ratio, under the exact MAC-weighted-upper-bound contract this
|
|
178
|
+
# comment documents -- measured a static report with one 1000-MAC
|
|
179
|
+
# eligible op and two 0-MAC CPU ops emitting `66.666...` here on the
|
|
180
|
+
# compiled path, precisely the op-count distortion MAC weighting exists
|
|
181
|
+
# to eliminate).
|
|
182
|
+
compute_on_npu_pct_max: float | None
|
|
183
|
+
# The REAL NPU-vs-CPU op-count placement ratio, 0-100 -- set only where a
|
|
184
|
+
# real placement was MEASURED: `basis: "compiled"` (`tan.model.check.
|
|
185
|
+
# _report_from_vela_compile`, from vela's own "CPU/NPU operators = N (P%)"
|
|
186
|
+
# summary counts), OR `basis: "bench"` in the ONE case that also counts as
|
|
187
|
+
# measured -- a bench point rebasing a report whose basis was ALREADY
|
|
188
|
+
# "compiled" (a real `--exact` compile ran alongside it and the two
|
|
189
|
+
# agreed; `tan.model.perf_apply._perf_point_report` carries this field
|
|
190
|
+
# from that pre-existing real placement, since a bench point has no
|
|
191
|
+
# per-operator placement of its own to contribute -- alp-sdk's real
|
|
192
|
+
# `metadata/model_perf/` schema carries none, tan-cli#1115, see
|
|
193
|
+
# `tan.model.perf`'s module docstring, "DEAD FIELDS"). `None` at
|
|
194
|
+
# `basis: "static-screen"` (no real compile to report a placement for)
|
|
195
|
+
# AND at `basis: "bench"` whenever it is NOT corroborating an already-
|
|
196
|
+
# "compiled" report -- the ordinary case, no `--exact` run at all -- a
|
|
197
|
+
# bench point alone never earns a non-`None` value here. Deliberately a
|
|
198
|
+
# SEPARATE field from `compute_on_npu_pct_max` above, not a reuse of it
|
|
199
|
+
# under a different meaning: an op-count ratio and a MAC-weighted ratio
|
|
200
|
+
# answer different questions and must never share one key a consumer could
|
|
201
|
+
# read as either.
|
|
202
|
+
npu_placement_pct_real: float | None = None
|
|
203
|
+
# cpu-certain ops with an unpriced (macs=0) MAC estimate -- excluded from
|
|
204
|
+
# compute_on_npu_pct_max's denominator, so a nonzero count here is a
|
|
205
|
+
# machine-readable caveat on that percentage: it can read 100.0 while real,
|
|
206
|
+
# uncosted CPU compute exists. Structured so the caveat survives into an
|
|
207
|
+
# envelope consumer that reads compute_on_npu_pct_max but doesn't render
|
|
208
|
+
# `notes` (prose). 0 whenever no such op exists, including every report
|
|
209
|
+
# variant that never reached scoring (format-not-accepted, no-table,
|
|
210
|
+
# empty-ops) -- there, no cpu-certain verdict was determined at all -- AND
|
|
211
|
+
# a `basis: "bench"` report rebased with no corroborating real placement
|
|
212
|
+
# (tan-cli#1115 PR review nit: this field is 0 there too, not because zero
|
|
213
|
+
# such ops were found but because `npu_coverage` itself was withheld to
|
|
214
|
+
# "undetermined" and nothing about placement was determined at all). A
|
|
215
|
+
# consumer CANNOT distinguish "zero uncosted CPU ops really exist" from
|
|
216
|
+
# "withheld" by this field alone -- read `npu_coverage`/`basis` first:
|
|
217
|
+
# `uncosted_cpu_op_count` only means what it says when `npu_coverage` is
|
|
218
|
+
# not `"undetermined"`.
|
|
219
|
+
uncosted_cpu_op_count: int = 0
|
|
220
|
+
ops: list[OpVerdict] = field(default_factory=list)
|
|
221
|
+
basis: str = "static-screen" # the only basis this module ever emits
|
|
222
|
+
confidence: str = "screening" # "certain" | "screening"
|
|
223
|
+
# THE FOOTPRINT AND LATENCY FIELDS, and what each `None` means.
|
|
224
|
+
#
|
|
225
|
+
# `None` is "not measured", never zero: a zero here is a measured zero, the
|
|
226
|
+
# same reading `metadata/model_perf/`'s own schema gives an omitted key.
|
|
227
|
+
# All six stay `None` at `basis: "static-screen"` -- a screen that walks
|
|
228
|
+
# operator NAMES against a table has no footprint to report and inventing
|
|
229
|
+
# one from it would be the estimate this whole vocabulary exists to keep
|
|
230
|
+
# separate from a measurement.
|
|
231
|
+
#
|
|
232
|
+
# `arena_bytes` / `req_sram_kib` are set at `basis: "compiled"` (from the
|
|
233
|
+
# `Blob` a real vela compile returned) and at `basis: "bench"` (from a
|
|
234
|
+
# matched perf point). Carrying them as FIELDS rather than only inside the
|
|
235
|
+
# prose note is what lets `tan.model.check` compare a customer's own
|
|
236
|
+
# compile against a bench point figure-by-figure instead of by parsing its
|
|
237
|
+
# own sentence back.
|
|
238
|
+
#
|
|
239
|
+
# The three latency fields and `perf_ref` are set at `basis: "bench"` --
|
|
240
|
+
# a compile reports no wall-clock, and only a bench point names the bench
|
|
241
|
+
# it was measured on (`capture.bench_id`, e.g. `"e1m-aen-evk-01"` -- a rig
|
|
242
|
+
# identifier, not a capture artefact's own citation) -- OR, informationally,
|
|
243
|
+
# on a `basis: "compiled"` report that names
|
|
244
|
+
# a disagreeing bench point in its notes
|
|
245
|
+
# (`tan.model.perf_apply.apply_perf_point`'s Decision 1: the customer's
|
|
246
|
+
# own compile still wins on coverage/arena/SRAM, but Alp Lab's own
|
|
247
|
+
# measured latency and traceable capture have no compiled counterpart to
|
|
248
|
+
# disagree WITH, so they ride alongside rather than being discarded --
|
|
249
|
+
# tan-cli#791 review item 3). A latency without `latency_runs` is a single
|
|
250
|
+
# shot rather than a measurement, which is why the count travels with the
|
|
251
|
+
# figures.
|
|
252
|
+
arena_bytes: int | None = None
|
|
253
|
+
req_sram_kib: int | None = None
|
|
254
|
+
latency_ms_mean: float | None = None
|
|
255
|
+
latency_ms_p95: float | None = None
|
|
256
|
+
latency_runs: int | None = None
|
|
257
|
+
perf_ref: str | None = None
|
|
258
|
+
notes: list[str] = field(default_factory=list)
|
|
259
|
+
# `Sram_Only` residency + arena fit against `board.yaml` (tan-cli#1288) --
|
|
260
|
+
# set by `tan.model.check._report_from_vela_compile` at `basis:
|
|
261
|
+
# "compiled"` (a real vela compile actually produced a blob to measure),
|
|
262
|
+
# and CARRIED FORWARD by `tan.model.perf_apply._perf_point_report`'s own
|
|
263
|
+
# `real_placement` rebase -- exactly the same gate that carries `ops`/
|
|
264
|
+
# `npu_placement_pct_real` -- when a matched bench point corroborates an
|
|
265
|
+
# already-`"compiled"` report. `None` at every construction site in THIS
|
|
266
|
+
# module (`basis: "static-screen"` -- no compiled blob exists to check)
|
|
267
|
+
# and at an UNCORROBORATED bench rebase (no alp-sdk has published a
|
|
268
|
+
# `model_perf/` tree yet, so this path is dormant today). See
|
|
269
|
+
# `tan.model.sram_fit` for the whole check.
|
|
270
|
+
sram_fit: SramFit | None = None
|
|
271
|
+
|
|
272
|
+
def __post_init__(self) -> None:
|
|
273
|
+
"""Refuse an illegitimate `(npu_coverage, basis)` pair at construction.
|
|
274
|
+
|
|
275
|
+
This is what makes `LEGITIMATE_COVERAGE_BY_BASIS` a constant
|
|
276
|
+
PRODUCTION reads rather than a list a test mirrors: all six
|
|
277
|
+
production construction sites (`analyze.py` x4, `tan.model.check.
|
|
278
|
+
_report_from_vela_compile`, `tan.model.perf_apply._perf_point_report`)
|
|
279
|
+
and every `dataclasses.replace` of a report run through here, so a new
|
|
280
|
+
branch that invents a word at a basis it does not belong to cannot
|
|
281
|
+
reach an envelope consumer that matches exhaustively on the pair
|
|
282
|
+
(`tan.core.model_check`, and the customers
|
|
283
|
+
`docs/model-check-static-screen.md` tells to do exactly that).
|
|
284
|
+
|
|
285
|
+
THE BLAST RADIUS, stated rather than left to "fails loudly": this is
|
|
286
|
+
not a traceback at the user. `tan model check` wraps each model in
|
|
287
|
+
`tan.commands.model_cmd._model_check_block`'s `except Exception`, so a
|
|
288
|
+
raise here becomes that ONE model's coded Issue and `exitCode: 1` --
|
|
289
|
+
the other models in the same `board.yaml` still report. Measured, on a
|
|
290
|
+
two-model board.yaml with the first model forced to an illegitimate
|
|
291
|
+
pair: `"ok": false`, `"exitCode": 1`, `issues[0]` =
|
|
292
|
+
`model.check-failed` / `model 'm': ValueError: illegitimate
|
|
293
|
+
(npu_coverage, basis) pair ('fits', 'static-screen') -- ...`, and
|
|
294
|
+
`data.models` still carrying the second model's full report.
|
|
295
|
+
|
|
296
|
+
That is the right trade for a false negative in the enumeration (one
|
|
297
|
+
report dropped, loudly, with the offending pair named) against the
|
|
298
|
+
alternative (a word the documented vocabulary does not contain,
|
|
299
|
+
shipped in a `data.models[].backends[]` block a consumer is told it
|
|
300
|
+
may match exhaustively). If the
|
|
301
|
+
enumeration is ever wrong in the OTHER direction -- too narrow for a
|
|
302
|
+
pair production really produces -- it costs a report, so widen it
|
|
303
|
+
here and document the row rather than deleting the check.
|
|
304
|
+
|
|
305
|
+
Deliberately validates the pair, NOT each field alone -- the whole
|
|
306
|
+
point is that `"fits"` is legitimate and `"full-eligible"` is not at
|
|
307
|
+
`basis: "compiled"`, while the reverse holds at `"static-screen"`.
|
|
308
|
+
Says nothing about MEANING -- that `undetermined` reads differently at
|
|
309
|
+
`"bench"` than at `"static-screen"` is prose, and stays prose."""
|
|
310
|
+
legitimate = LEGITIMATE_COVERAGE_BY_BASIS.get(self.basis)
|
|
311
|
+
if legitimate is None:
|
|
312
|
+
raise ValueError(
|
|
313
|
+
f"unknown basis {self.basis!r} -- BackendReport bases are "
|
|
314
|
+
f"{sorted(LEGITIMATE_COVERAGE_BY_BASIS)}")
|
|
315
|
+
if self.npu_coverage not in legitimate:
|
|
316
|
+
raise ValueError(
|
|
317
|
+
f"illegitimate (npu_coverage, basis) pair "
|
|
318
|
+
f"({self.npu_coverage!r}, {self.basis!r}) -- legitimate "
|
|
319
|
+
f"npu_coverage at basis {self.basis!r}: {sorted(legitimate)}")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def resolve_ethos_u_variant(sku: str, *, metadata_root: Path) -> str | None:
|
|
323
|
+
"""Read `inference.ethos_u_variant` off the SoM preset for @sku. None for
|
|
324
|
+
a SoM preset with no such field (non-Ethos-U SoMs -- drpai/deepx_dxm1
|
|
325
|
+
presets carry no per-instance Ethos-U variant), or for a preset that
|
|
326
|
+
doesn't exist on disk (callers that need a hard failure for a bad SKU
|
|
327
|
+
should resolve targets first; this function stays soft-fail, matching
|
|
328
|
+
`tan.soc_ref.resolve_soc_path`'s stance). Also soft-fails (None, never a
|
|
329
|
+
raise) for a preset document, `inference` block, or `ethos_u_variant`
|
|
330
|
+
value that parses to the wrong shape -- live on `tan model check`
|
|
331
|
+
(`check.py:155`, `check.py:537`), same #957/#965/#969 defect class,
|
|
332
|
+
tan-cli#979 review finding 2: `inference: 7` and a scalar top-level
|
|
333
|
+
preset document (e.g. a YAML file containing only `7`) both raised
|
|
334
|
+
`AttributeError: 'int' object has no attribute 'get'` (measured), and
|
|
335
|
+
`ethos_u_variant: 7` returned the int straight into
|
|
336
|
+
`analyze_backend(variant: str | None)` uncaught."""
|
|
337
|
+
preset_path = Path(metadata_root) / "e1m_modules" / f"{sku}.yaml"
|
|
338
|
+
if not preset_path.is_file():
|
|
339
|
+
return None
|
|
340
|
+
import yaml # noqa: PLC0415 -- deferred (tan-cli#810); see sdk_cmd's `_releases_opener`
|
|
341
|
+
|
|
342
|
+
preset = yaml.safe_load(preset_path.read_text(encoding="utf-8"))
|
|
343
|
+
# A preset document that doesn't even parse to a dict (a bare scalar or
|
|
344
|
+
# list at the YAML top level) can't be walked by the `.get()`s below --
|
|
345
|
+
# treat it exactly like a missing preset rather than let it reach
|
|
346
|
+
# `.get()` and raise.
|
|
347
|
+
if not isinstance(preset, dict):
|
|
348
|
+
return None
|
|
349
|
+
# `isinstance` check, not a `.get(..., {})` default: a preset carrying an
|
|
350
|
+
# explicit but EMPTY `inference:` block parses to `{"inference": None}`,
|
|
351
|
+
# and the dict-literal default only fires when the key is absent
|
|
352
|
+
# entirely -- `.get("inference", {})` still returns None there, and
|
|
353
|
+
# `.get()` on None raises AttributeError, contradicting this function's
|
|
354
|
+
# own soft-fail docstring. A non-dict, non-null `inference:` (e.g. `7`)
|
|
355
|
+
# needs the same treatment, which a bare `or {}` alone does not give it.
|
|
356
|
+
inference = preset.get("inference")
|
|
357
|
+
if not isinstance(inference, dict):
|
|
358
|
+
return None
|
|
359
|
+
ethos_u_variant = inference.get("ethos_u_variant")
|
|
360
|
+
return ethos_u_variant if isinstance(ethos_u_variant, str) else None
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _load_table(path: Path) -> dict | None:
|
|
364
|
+
try:
|
|
365
|
+
doc = json.loads(path.read_text(encoding="utf-8"))
|
|
366
|
+
except (OSError, ValueError):
|
|
367
|
+
return None
|
|
368
|
+
# A table file that parses but is not a JSON object (e.g. a bare array)
|
|
369
|
+
# must be rejected HERE -- every caller treats a non-None return as
|
|
370
|
+
# something it can `.get()` off, and letting a list or scalar through
|
|
371
|
+
# turns a downstream `.get()` into an uncaught AttributeError instead of
|
|
372
|
+
# the clean "no table" this function promises.
|
|
373
|
+
return doc if isinstance(doc, dict) else None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _table_variant(doc: dict) -> str:
|
|
377
|
+
"""@doc's `applies_to.variant`, normalised to `""` for any shape that
|
|
378
|
+
isn't a plain string reachable through a dict `applies_to`.
|
|
379
|
+
|
|
380
|
+
`metadata/npu_ops/**` has a schema, `npu-ops-v1.schema.json` (alp-sdk
|
|
381
|
+
`dev`, alp-sdk#1470), and `_resolve_table` enforces it on read when the
|
|
382
|
+
bound SDK ships a usable one (tan-cli#1298). This isinstance guard stays
|
|
383
|
+
as defence in depth, for three reasons: it is what protects a bound SDK
|
|
384
|
+
whose schema is absent or cannot be loaded (tan-cli#969 added it before
|
|
385
|
+
any schema existed); `_resolve_table` reads the variant through it BEFORE
|
|
386
|
+
schema-validating the matching table; and tan-cli#965 showed a
|
|
387
|
+
schema-valid document can still be narrower than a consumer assumes.
|
|
388
|
+
|
|
389
|
+
`.get()` off a non-dict `applies_to` (e.g. `applies_to: 7`) raises
|
|
390
|
+
`AttributeError: 'int' object has no attribute 'get'`, and `.split()` on
|
|
391
|
+
the caller's side off a non-string `variant` (e.g. `variant: 7`) raises
|
|
392
|
+
`AttributeError: 'int' object has no attribute 'split'` -- same defect
|
|
393
|
+
class as the #957/#965 family, same `_load_table` precedent immediately
|
|
394
|
+
above (`analyze.py:204-214`) for why a malformed table is skipped rather
|
|
395
|
+
than crashing the whole resolve. `or {}` alone (the pre-#969 guard) only
|
|
396
|
+
covered an explicit-but-null `applies_to:`; it did not cover a non-dict
|
|
397
|
+
scalar, which still reached `.get()`.
|
|
398
|
+
|
|
399
|
+
`""` is the identical sentinel a genuinely-absent field already
|
|
400
|
+
produces: the caller's `variant in _table_variant(doc).split("-")`
|
|
401
|
+
then simply fails to match, the same "undetermined, not a fabricated
|
|
402
|
+
negative" outcome `_resolve_table`'s own docstring already promises for
|
|
403
|
+
"no table covers @variant" -- no fourth behaviour invented."""
|
|
404
|
+
applies_to = doc.get("applies_to")
|
|
405
|
+
if not isinstance(applies_to, dict):
|
|
406
|
+
applies_to = {}
|
|
407
|
+
table_variant = applies_to.get("variant", "")
|
|
408
|
+
return table_variant if isinstance(table_variant, str) else ""
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _table_supported_ops(doc: dict) -> list[str] | None:
|
|
412
|
+
"""@doc's `supported_ops`, validated to a list of `str`. `None` for any
|
|
413
|
+
other shape -- the caller (`analyze_backend`) treats that identically to
|
|
414
|
+
a table that failed to resolve at all, never as evidence to score
|
|
415
|
+
against (tan-cli#979 review finding 1, on top of #969's `applies_to`/
|
|
416
|
+
`variant` guards above).
|
|
417
|
+
|
|
418
|
+
`_score_ops` used to read `set(doc.get("supported_ops", []))` straight
|
|
419
|
+
off the same `metadata/npu_ops/**` document `_table_variant` sanitises
|
|
420
|
+
above, a few lines below on the same `analyze_backend` call path (a bound
|
|
421
|
+
SDK whose schema is absent or unusable still reaches this guard
|
|
422
|
+
unvalidated, and the schema does not replace it either, #965).
|
|
423
|
+
`supported_ops: 7` raised `TypeError: 'int' object is not
|
|
424
|
+
iterable`; `supported_ops: null` raised the same as `'NoneType' object
|
|
425
|
+
is not iterable` -- but `supported_ops: "CONV_2D"` raised NOTHING:
|
|
426
|
+
`set("CONV_2D")` silently built a set of seven characters no real
|
|
427
|
+
operator name ever matches, so every op came back `cpu-only`/
|
|
428
|
+
`cpu-certain` -- a fabricated negative manufactured from malformed data,
|
|
429
|
+
exactly the outcome this module's own docstring (`analyze.py:18-23`)
|
|
430
|
+
names as the worst one it exists to prevent. A crash at least announces
|
|
431
|
+
itself; that one would have shipped a wrong verdict silently.
|
|
432
|
+
|
|
433
|
+
A genuinely-absent key defaults to `[]` (a table that legitimately
|
|
434
|
+
supports no operators) and passes straight through -- only a PRESENT but
|
|
435
|
+
wrong-shaped field is rejected."""
|
|
436
|
+
supported_ops = doc.get("supported_ops", [])
|
|
437
|
+
if not isinstance(supported_ops, list):
|
|
438
|
+
return None
|
|
439
|
+
if not all(isinstance(op, str) for op in supported_ops):
|
|
440
|
+
return None
|
|
441
|
+
return supported_ops
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# WHY `os.listdir` AND NOT `Path.glob` HERE (tan-cli#1132, the same swap
|
|
445
|
+
# tan-cli#1127 made in `new_som_cmd._known_board_names`, measured the same
|
|
446
|
+
# way -- non-root, on 3.12.3 / 3.13.15 / 3.14.7 side by side).
|
|
447
|
+
#
|
|
448
|
+
# `_resolve_table` used to open with `if not table_dir.is_dir(): return None`
|
|
449
|
+
# and then `sorted(table_dir.glob("*.json"))` with no guard at all, so a
|
|
450
|
+
# permission-denied ANCESTOR of `metadata/npu_ops/<backend>/` raised
|
|
451
|
+
# `PermissionError` straight out of this function on 3.12.3 and 3.13.15 and
|
|
452
|
+
# returned `None` on 3.14.7 -- the docstring's "None when the backend has no
|
|
453
|
+
# table directory at all" held on exactly one of the three. Measured:
|
|
454
|
+
#
|
|
455
|
+
# primitive self-denied ancestor-denied
|
|
456
|
+
# Path.is_dir() True / True / True RAISE / RAISE / False
|
|
457
|
+
# list(Path.glob("*.json")) [] / [] / [] RAISE / [] / []
|
|
458
|
+
# os.listdir() RAISE / RAISE / RAISE RAISE / RAISE / RAISE
|
|
459
|
+
# (3.12.3 / 3.13.15 / 3.14.7)
|
|
460
|
+
#
|
|
461
|
+
# `os.listdir` is the only one of the three that answers the same way on all
|
|
462
|
+
# three interpreters and both shapes, which is what lets ONE `except OSError`
|
|
463
|
+
# below carry the whole contract -- an `except` around `Path.glob` would be
|
|
464
|
+
# dead code on 3.13.15 and 3.14.7 (and, for the self-denied shape, on 3.12.3
|
|
465
|
+
# too). A missing directory and a non-directory in its place come through the
|
|
466
|
+
# same handler: `FileNotFoundError`/`NotADirectoryError` are `OSError`, and
|
|
467
|
+
# all of them mean `None` here.
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _is_table_file(name: str) -> bool:
|
|
471
|
+
"""True if @name is a support-table filename -- `.json`, matched
|
|
472
|
+
case-sensitively on POSIX and case-INSENSITIVELY on Windows, exactly as
|
|
473
|
+
the `table_dir.glob("*.json")` this replaced did (tan-cli#1132; see
|
|
474
|
+
`shapes.matches_glob_suffix` for the rule and tan-cli#1127 review round
|
|
475
|
+
2 for why a plain `endswith` would have silently narrowed it)."""
|
|
476
|
+
return matches_glob_suffix(name, ".json")
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _resolve_table(metadata_root: Path, backend: str, variant: str | None,
|
|
480
|
+
findings: list[str] | None = None) -> tuple[Path, dict] | None:
|
|
481
|
+
"""Pick the `metadata/npu_ops/<backend>/<variant>@<toolchain>-<ver>.json`
|
|
482
|
+
whose `applies_to.variant` covers @variant. None when the backend's table
|
|
483
|
+
directory is missing, unreadable, or not a directory at all (e.g.
|
|
484
|
+
`deepx_dxm1`, by decision), or when no table in it covers @variant -- all
|
|
485
|
+
of these are the caller's `undetermined`, never a negative verdict
|
|
486
|
+
manufactured from absent data, and none of them is an error this static
|
|
487
|
+
screen should raise at its caller.
|
|
488
|
+
|
|
489
|
+
@findings, when given, collects user-facing notes for the report
|
|
490
|
+
(tan-cli#1298): one per MATCHING table that failed `npu-ops-v1.schema.json`
|
|
491
|
+
(skipped like a malformed file; the search continues), or one disclosing
|
|
492
|
+
that the schema is absent or unusable so the table about to be used was
|
|
493
|
+
NOT validated. See `npu_ops_validate` for the order and why. It is an
|
|
494
|
+
out-parameter rather than a return value so the `(path, doc) | None`
|
|
495
|
+
shape every caller already unpacks does not change."""
|
|
496
|
+
table_dir = Path(metadata_root) / "npu_ops" / backend
|
|
497
|
+
try:
|
|
498
|
+
entries = os.listdir(table_dir)
|
|
499
|
+
except OSError:
|
|
500
|
+
return None
|
|
501
|
+
candidates = sorted(table_dir / entry for entry in entries if _is_table_file(entry))
|
|
502
|
+
if not candidates:
|
|
503
|
+
return None
|
|
504
|
+
notes = findings if findings is not None else []
|
|
505
|
+
validator = TableValidator(metadata_root, table_dir)
|
|
506
|
+
|
|
507
|
+
if variant is None:
|
|
508
|
+
# No per-instance variant to resolve (e.g. drpai, which ships exactly
|
|
509
|
+
# one table today): unambiguous only when there is exactly one table.
|
|
510
|
+
if len(candidates) == 1:
|
|
511
|
+
doc = _load_table(candidates[0])
|
|
512
|
+
if doc is not None and validator.accepts(doc, candidates[0], notes):
|
|
513
|
+
return candidates[0], doc
|
|
514
|
+
return None
|
|
515
|
+
for path in candidates:
|
|
516
|
+
doc = _load_table(path)
|
|
517
|
+
if doc is None:
|
|
518
|
+
continue
|
|
519
|
+
# The variant is read off a not-yet-validated document -- safe, since
|
|
520
|
+
# `_table_variant` returns "" for any wrong shape, so such a table can
|
|
521
|
+
# only fail to match. Only a MATCHING table is schema-validated; a
|
|
522
|
+
# schema-invalid one is skipped (and noted) and the search goes on.
|
|
523
|
+
if variant in _table_variant(doc).split("-") and validator.accepts(doc, path, notes):
|
|
524
|
+
return path, doc
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _with_table_notes(report: BackendReport, table_notes: list[str]) -> BackendReport:
|
|
529
|
+
"""@report with the resolver's schema findings appended to `notes`. Only
|
|
530
|
+
the static-screen reports built here carry them: the `--exact` compiled
|
|
531
|
+
report (`check._report_from_vela_compile`) and the bench-point report
|
|
532
|
+
(`perf_apply._perf_point_report`) build their own `notes` and drop them."""
|
|
533
|
+
return replace(report, notes=[*report.notes, *table_notes]) if table_notes else report
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _format_not_accepted_report(backend: str, src_format: str, ops: Sequence[OpDesc],
|
|
537
|
+
variant: str | None) -> BackendReport:
|
|
538
|
+
verdicts = [OpVerdict(op=o.op, status="unknown", reason="format-not-accepted")
|
|
539
|
+
for o in ops]
|
|
540
|
+
return BackendReport(
|
|
541
|
+
backend=backend, variant=variant, table=None,
|
|
542
|
+
npu_coverage=COVERAGE_WITHHELD, compute_on_npu_pct_max=None, ops=verdicts,
|
|
543
|
+
notes=[f"{backend} does not ingest {src_format!r} source models; "
|
|
544
|
+
f"no score computed. This is not a verdict on the model, only "
|
|
545
|
+
f"on the format/backend pairing."],
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _no_table_report(backend: str, ops: Sequence[OpDesc], variant: str | None) -> BackendReport:
|
|
550
|
+
verdicts = [OpVerdict(op=o.op, status="unknown", reason="no-table-for-backend",
|
|
551
|
+
macs=o.macs) for o in ops]
|
|
552
|
+
return BackendReport(
|
|
553
|
+
backend=backend, variant=variant, table=None,
|
|
554
|
+
npu_coverage=COVERAGE_WITHHELD, compute_on_npu_pct_max=None, ops=verdicts,
|
|
555
|
+
notes=["no NPU-ops support table for this backend/variant -- absence "
|
|
556
|
+
"of data, not evidence of no support."],
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _empty_ops_report(backend: str, table_path: Path, variant: str | None) -> BackendReport:
|
|
561
|
+
return BackendReport(
|
|
562
|
+
backend=backend, variant=variant, table=str(table_path),
|
|
563
|
+
npu_coverage=COVERAGE_WITHHELD, compute_on_npu_pct_max=None, ops=[],
|
|
564
|
+
notes=["no operators were extracted for this source; nothing to "
|
|
565
|
+
"score, so no coverage verdict is reported."],
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _score_ops(ops: Sequence[OpDesc], doc: dict) -> tuple[list[OpVerdict], int, int]:
|
|
570
|
+
"""Per-op npu-eligible/cpu-certain verdicts against @doc's `supported_ops`,
|
|
571
|
+
plus the running total/eligible MAC totals MAC-weighting needs.
|
|
572
|
+
|
|
573
|
+
@doc is assumed pre-validated by `analyze_backend` via
|
|
574
|
+
`_table_supported_ops` before this is ever called -- a malformed
|
|
575
|
+
`supported_ops` is routed to `_no_table_report` instead of reaching
|
|
576
|
+
here (tan-cli#979 review finding 1: `set()` of a non-list, or of a list
|
|
577
|
+
containing a non-str, is exactly the crash/fabricated-negative class
|
|
578
|
+
this module exists to keep out, so this function must never be called
|
|
579
|
+
with an unvalidated @doc)."""
|
|
580
|
+
supported = set(doc.get("supported_ops", []))
|
|
581
|
+
verdicts: list[OpVerdict] = []
|
|
582
|
+
total_macs = 0
|
|
583
|
+
eligible_macs = 0
|
|
584
|
+
for o in ops:
|
|
585
|
+
total_macs += o.macs
|
|
586
|
+
if o.op in supported:
|
|
587
|
+
verdicts.append(OpVerdict(op=o.op, status="npu-eligible",
|
|
588
|
+
reason="constraint-unchecked", macs=o.macs))
|
|
589
|
+
eligible_macs += o.macs
|
|
590
|
+
else:
|
|
591
|
+
verdicts.append(OpVerdict(op=o.op, status="cpu-certain",
|
|
592
|
+
reason="op-not-in-table", macs=o.macs))
|
|
593
|
+
return verdicts, total_macs, eligible_macs
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _coverage_label(verdicts: list[OpVerdict]) -> str:
|
|
597
|
+
all_eligible = all(v.status == "npu-eligible" for v in verdicts)
|
|
598
|
+
any_eligible = any(v.status == "npu-eligible" for v in verdicts)
|
|
599
|
+
if all_eligible:
|
|
600
|
+
return "full-eligible"
|
|
601
|
+
return "partial" if any_eligible else "cpu-only"
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _uncosted_op_count(verdicts: list[OpVerdict]) -> int:
|
|
605
|
+
"""Count of cpu-certain ops `_estimate_macs` could not price (macs=0) --
|
|
606
|
+
also the source of the `uncosted_cpu_op_count` report field, so the note
|
|
607
|
+
below and the structured field always agree."""
|
|
608
|
+
return sum(1 for v in verdicts if v.status == "cpu-certain" and v.macs == 0)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _uncosted_macs_note(uncosted: int, pct: float | None) -> str | None:
|
|
612
|
+
"""compute_on_npu_pct_max's denominator only counts ops `_estimate_macs`
|
|
613
|
+
could price (conv/dense); a cpu-certain op outside that set (macs=0)
|
|
614
|
+
leaves the denominator entirely, so the percentage can read 100.0 while
|
|
615
|
+
real, un-priced CPU compute exists and `npu_coverage` still says
|
|
616
|
+
"partial". Never clamp for this -- clamping hides the exact gap this
|
|
617
|
+
module exists to surface -- return a note instead, or None when there is
|
|
618
|
+
nothing uncosted (or no MAC-weighted number was computed at all)."""
|
|
619
|
+
if pct is None or not uncosted:
|
|
620
|
+
return None
|
|
621
|
+
return (f"{uncosted} cpu-certain op(s) carry no static MAC estimate (outside "
|
|
622
|
+
f"the conv/dense-only estimator) and are excluded from "
|
|
623
|
+
f"compute_on_npu_pct_max's denominator -- the reported percentage "
|
|
624
|
+
f"can overstate NPU-bound compute share when those ops are "
|
|
625
|
+
f"significant in practice.")
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def analyze_backend(*, backend: str, src_format: str, ops: Sequence[OpDesc],
|
|
629
|
+
metadata_root: Path, variant: str | None = None) -> BackendReport:
|
|
630
|
+
"""Static-screen @ops (already extracted from the model source) against
|
|
631
|
+
@backend's resolved support table.
|
|
632
|
+
|
|
633
|
+
Order of operations, deliberately: (1) the format gate -- does @backend
|
|
634
|
+
even ingest @src_format -- before (2) table resolution, before (3) the
|
|
635
|
+
per-op walk. Scoring a format a backend cannot ingest is a category error,
|
|
636
|
+
not a low-confidence answer, so it short-circuits before any table load."""
|
|
637
|
+
if not _known_backend(backend):
|
|
638
|
+
raise ValueError(f"unknown backend {backend!r}")
|
|
639
|
+
|
|
640
|
+
if not _accepts(backend, src_format):
|
|
641
|
+
return _format_not_accepted_report(backend, src_format, ops, variant)
|
|
642
|
+
|
|
643
|
+
table_notes: list[str] = []
|
|
644
|
+
resolved = _resolve_table(metadata_root, backend, variant, table_notes)
|
|
645
|
+
if resolved is not None:
|
|
646
|
+
# The table must speak the SAME operator vocabulary as what will be
|
|
647
|
+
# compared against it -- @ops' own op_namespace when there are ops to
|
|
648
|
+
# compare (never @src_format, which is caller-supplied and
|
|
649
|
+
# independent of what extraction really produced), or, when @ops is
|
|
650
|
+
# empty and there is no extracted vocabulary to check, @src_format as
|
|
651
|
+
# the best available surrogate. Always run this -- even for the
|
|
652
|
+
# empty-ops case, where nothing gets scored either way -- because
|
|
653
|
+
# citing a table whose op_namespace demonstrably disagrees with the
|
|
654
|
+
# only vocabulary evidence available is misleading regardless of
|
|
655
|
+
# whether anything was scored against it.
|
|
656
|
+
actual_namespace = ops[0].op_namespace if ops else src_format
|
|
657
|
+
if resolved[1].get("op_namespace") != actual_namespace:
|
|
658
|
+
resolved = None
|
|
659
|
+
elif _table_supported_ops(resolved[1]) is None:
|
|
660
|
+
# tan-cli#979 review finding 1: a table with a well-formed
|
|
661
|
+
# `applies_to`/`op_namespace` (i.e. one that survived every
|
|
662
|
+
# check above -- exactly the table `_score_ops` would otherwise
|
|
663
|
+
# be handed) but a malformed `supported_ops` is unscoreable, not
|
|
664
|
+
# a table that legitimately supports nothing. Route it through
|
|
665
|
+
# the same "no usable table" outcome as a namespace mismatch --
|
|
666
|
+
# `undetermined`, never a verdict manufactured from the bad
|
|
667
|
+
# field -- rather than reaching `_score_ops` at all.
|
|
668
|
+
resolved = None
|
|
669
|
+
|
|
670
|
+
if resolved is None:
|
|
671
|
+
return _with_table_notes(_no_table_report(backend, ops, variant), table_notes)
|
|
672
|
+
|
|
673
|
+
table_path, doc = resolved
|
|
674
|
+
if not ops:
|
|
675
|
+
return _with_table_notes(_empty_ops_report(backend, table_path, variant), table_notes)
|
|
676
|
+
|
|
677
|
+
verdicts, total_macs, eligible_macs = _score_ops(ops, doc)
|
|
678
|
+
pct = (100.0 * eligible_macs / total_macs) if total_macs > 0 else None
|
|
679
|
+
notes = [f"static screen ({doc.get('stance', 'screening')}): operator-name "
|
|
680
|
+
f"membership against {table_path.name} only. Eligible ops still "
|
|
681
|
+
f"carry unchecked quantization/shape/dtype constraints this check "
|
|
682
|
+
f"cannot verify -- the model will run either way, an unsupported "
|
|
683
|
+
f"op falls back to the CPU silently rather than failing. Only a "
|
|
684
|
+
f"real compile proves NPU execution."]
|
|
685
|
+
uncosted = _uncosted_op_count(verdicts)
|
|
686
|
+
uncosted_note = _uncosted_macs_note(uncosted, pct)
|
|
687
|
+
if uncosted_note:
|
|
688
|
+
notes.append(uncosted_note)
|
|
689
|
+
|
|
690
|
+
return BackendReport(
|
|
691
|
+
backend=backend, variant=variant, table=str(table_path),
|
|
692
|
+
npu_coverage=_coverage_label(verdicts), compute_on_npu_pct_max=pct,
|
|
693
|
+
uncosted_cpu_op_count=uncosted, ops=verdicts, notes=[*notes, *table_notes],
|
|
694
|
+
)
|