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/targets.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Derive .alpmodel compile targets from a SoM SKU (silicon-determined).
|
|
3
|
+
|
|
4
|
+
Targets come from the host SoC's npus[] *and* from any on-module discrete
|
|
5
|
+
accelerator (e.g. the DEEPX DX-M1 on V2M SoMs). A discrete accelerator is any
|
|
6
|
+
*other* SoC JSON whose variants[].alp_module_skus lists this SKU -- so the SoC
|
|
7
|
+
JSON's alp_module_skus stays the single source of truth (no hardcoded
|
|
8
|
+
backend->silicon map)."""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import NamedTuple
|
|
14
|
+
|
|
15
|
+
from tan.soc_ref import resolve_soc_path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class TargetSpec:
|
|
20
|
+
backend: str # cpu | ethos_u | drpai | deepx_dxm1
|
|
21
|
+
silicon_ref: str # SoC ref e.g. "alif:ensemble:e7" | "deepx:dx:m1" | "*"
|
|
22
|
+
accel_config: str # vela accel-config e.g. "ethos-u55-256"; "" when N/A
|
|
23
|
+
# The SILICON's vela invocation profile, from the SoC spec's
|
|
24
|
+
# `npu_toolchain.vela` block (alp-sdk #1470) -- ethos_u targets only, None
|
|
25
|
+
# everywhere else. Optional with None defaults so no existing TargetSpec
|
|
26
|
+
# construction site has to change, and so a SoC spec that predates the
|
|
27
|
+
# block resolves to "pass no profile flag", which is exactly today's
|
|
28
|
+
# behaviour rather than a guess.
|
|
29
|
+
vela_memory_mode: str | None = None # Arm built-in, e.g. "Sram_Only"
|
|
30
|
+
vela_system_config: str | None = None # ONLY when it needs no vendor config
|
|
31
|
+
# The other half of that block: the vendor-gated System_Config and the file
|
|
32
|
+
# that defines it. Carried SEPARATELY from `vela_system_config` -- which
|
|
33
|
+
# stays "safe to put on a command line alone" by construction -- because
|
|
34
|
+
# these two are legal only TOGETHER and only alongside a `--config` the
|
|
35
|
+
# customer supplies through `ALP_VELA_CONFIG`
|
|
36
|
+
# (`tan.model.adapters.ethos_u`). None for every part shipped today: no SoC
|
|
37
|
+
# spec names a `system_config` at all.
|
|
38
|
+
vela_vendor_system_config: str | None = None
|
|
39
|
+
# The vendor `.ini`'s FILENAME as metadata declares it
|
|
40
|
+
# (`vendor_config_filename`, e.g. `ensemble_vela.ini`) -- a DIAGNOSTIC
|
|
41
|
+
# fact, never a path: `tan.model.adapters.ethos_u`'s footprint refusal
|
|
42
|
+
# names it to explain why tan cannot resolve a complete profile for this
|
|
43
|
+
# part. A part whose metadata declares none gets no file named, so no
|
|
44
|
+
# customer is ever sent after another vendor's download.
|
|
45
|
+
vela_vendor_config_filename: str | None = None
|
|
46
|
+
# Does this SoC declare ANY external DRAM interface
|
|
47
|
+
# (`external_memory_interfaces[].kind`)? `False` is the machine-checkable
|
|
48
|
+
# evidence behind the zero-SRAM refusal: vela's default profiles are
|
|
49
|
+
# DRAM-backed, and a working set placed in DRAM on a part that declares no
|
|
50
|
+
# DRAM interface describes memory the module does not have. `None` means
|
|
51
|
+
# the spec says nothing, and an unknown must never be reported as a "no"
|
|
52
|
+
# -- see `_soc_declares_dram`.
|
|
53
|
+
soc_declares_dram: bool | None = None
|
|
54
|
+
# Which CPU core drives this accelerator, from the SoC spec's own
|
|
55
|
+
# `npus[].paired_core` -- `m55_hp` for the E8's high-perf Ethos-U55,
|
|
56
|
+
# `m55_he` for its high-efficiency one. `None` where the spec declares no
|
|
57
|
+
# pairing (the E8's Ethos-U85 today), and that `None` is a real answer, not
|
|
58
|
+
# a gap to fill in: the metadata does not know which core drives it, so
|
|
59
|
+
# nothing downstream may infer one. A bench-measured perf point's identity
|
|
60
|
+
# includes the core (alp-sdk `f724d3e4`), and this is the only sourced
|
|
61
|
+
# answer tan has for it.
|
|
62
|
+
paired_core: str | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _VelaProfile(NamedTuple):
|
|
66
|
+
"""What one SoC spec's `npu_toolchain.vela` block resolves to, split by
|
|
67
|
+
what is SAFE TO PASS: `system_config` is an Arm built-in that vela accepts
|
|
68
|
+
on its own, `vendor_system_config` is a name that exists only inside
|
|
69
|
+
`vendor_config_filename` and is rc=1 without it."""
|
|
70
|
+
memory_mode: str | None
|
|
71
|
+
system_config: str | None
|
|
72
|
+
vendor_system_config: str | None
|
|
73
|
+
vendor_config_filename: str | None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
#: Substrings that make an `external_memory_interfaces[].kind` a DRAM
|
|
77
|
+
#: interface, matched case-insensitively against the declared kind. Covers
|
|
78
|
+
#: every DRAM spelling in metadata today -- `LPDDR4/4X` (renesas:rzv2n:n44), `LPDDR4X / LPDDR5` (deepx:dx:m1) -- and nothing else:
|
|
79
|
+
#: `HexSPI`, `OctalSPI`, `FlexSPI`, `xSPI` and `SD/eMMC` (the ONLY kinds the
|
|
80
|
+
#: six Alif Ensemble parts declare) are not DRAM.
|
|
81
|
+
#:
|
|
82
|
+
#: Matched on `kind` alone and NOT on the interface's `supports` list, which
|
|
83
|
+
#: is where the Alif parts carry `HyperBus`. A HyperBus port can host HyperRAM,
|
|
84
|
+
#: but that is a populated-device question this spec does not answer, and vela's
|
|
85
|
+
#: `Dram` memory area is not it -- reading `supports` here would turn an
|
|
86
|
+
#: attachable-bus fact into a "this part has DRAM" claim nothing sourced.
|
|
87
|
+
_DRAM_INTERFACE_TOKENS = ("DDR", "DRAM")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _soc_declares_dram(soc: dict) -> bool | None:
|
|
91
|
+
"""Does this SoC spec declare an external DRAM interface? `None` when it
|
|
92
|
+
declares no `external_memory_interfaces` at all.
|
|
93
|
+
|
|
94
|
+
THE `None` IS THE POINT, and it fails closed. The consumer
|
|
95
|
+
(`tan.model.adapters.ethos_u._refuse_zero_sram_footprint`) states "this SoC
|
|
96
|
+
declares no DRAM interface" as EVIDENCE in a refusal a customer sizes
|
|
97
|
+
hardware from, so it may only ever say that on an explicit `False`. A spec
|
|
98
|
+
that carries no interface list has not declared the absence of DRAM; it has
|
|
99
|
+
said nothing, and saying nothing back is the only honest answer. Same
|
|
100
|
+
reading for an EMPTY list, which is a stub far more often than a positive
|
|
101
|
+
statement that a part has no external memory at all."""
|
|
102
|
+
interfaces = soc.get("external_memory_interfaces")
|
|
103
|
+
if not isinstance(interfaces, list) or not interfaces:
|
|
104
|
+
return None
|
|
105
|
+
for iface in interfaces:
|
|
106
|
+
kind = str(iface.get("kind", "")) if isinstance(iface, dict) else ""
|
|
107
|
+
if any(token in kind.upper() for token in _DRAM_INTERFACE_TOKENS):
|
|
108
|
+
return True
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _vela_profile(soc: dict) -> _VelaProfile:
|
|
113
|
+
"""How to invoke vela for THIS SoC, read from its `npu_toolchain.vela`
|
|
114
|
+
block. All-None for a spec that carries no block -- never a guessed
|
|
115
|
+
profile; a wrong one compiles a command stream for memory the module does
|
|
116
|
+
not have.
|
|
117
|
+
|
|
118
|
+
THE SYSTEM_CONFIG GUARD IS LOAD-BEARING. `--memory-mode` values are Arm
|
|
119
|
+
built-ins (the SoC schema constrains `memory_mode` to the `[Memory_Mode.*]`
|
|
120
|
+
sections `ethos-u-vela` 5.1.0 ships), so passing one always works. A
|
|
121
|
+
`System_Config` name usually is NOT: the Alif Ensemble parts' own
|
|
122
|
+
`Ethos_U85_SRAM_Only` / `RTSS_HE_SRAM_Only` live only in the proprietary
|
|
123
|
+
`ensemble_vela.ini`, and handing vela a section it cannot resolve is a hard
|
|
124
|
+
rc=1, measured verbatim:
|
|
125
|
+
|
|
126
|
+
ethosu.vela.errors.CliOptionError: 'Error: Incorrect argument to CLI
|
|
127
|
+
option --system-config=Ethos_U85_SRAM_Only: Section
|
|
128
|
+
System_Config.Ethos_U85_SRAM_Only not found in Vela config file'
|
|
129
|
+
|
|
130
|
+
So a `system_config` is carried ONLY when the block declares
|
|
131
|
+
`system_config_requires_vendor_config` EXPLICITLY FALSE. It fails CLOSED,
|
|
132
|
+
not open: an ABSENT key withholds the name, exactly as `true` does. The
|
|
133
|
+
defence for reading it as falsy was that alp-sdk's SoC schema makes the key
|
|
134
|
+
`required` -- true today, but enforced in the OTHER repo, against a
|
|
135
|
+
checkout tan binds at an arbitrary version (`ALP_SDK_ROOT`, any tag or
|
|
136
|
+
branch a user has on disk). That is the same cross-repo drift class
|
|
137
|
+
`tests/gates/test_planner_relocation_freshness.py` exists for, and here the
|
|
138
|
+
failure mode is not a stale hash: an unannotated `system_config` would walk
|
|
139
|
+
straight onto vela's command line and turn a build into a hard rc=1. `is
|
|
140
|
+
False` is the only reading under which a spec that never made the promise
|
|
141
|
+
cannot make it by omission.
|
|
142
|
+
|
|
143
|
+
An ABSENT `system_config` is likewise "nothing to pass", never "no vendor
|
|
144
|
+
config needed": no Alif spec names one today (an Alif System_Config
|
|
145
|
+
describes one CORE SUBSYSTEM, not a die), so this branch yields None for
|
|
146
|
+
every part currently shipped -- correct, and the mechanism is here for the
|
|
147
|
+
spec that eventually does carry a built-in one.
|
|
148
|
+
|
|
149
|
+
A vendor-gated name is WITHHELD FROM `system_config` AND CARRIED ON
|
|
150
|
+
`vendor_system_config`, rather than dropped. The two fields are exactly
|
|
151
|
+
"safe alone" versus "legal only with its `.ini`": the adapter may put the
|
|
152
|
+
first on a command line unconditionally, and the second only alongside a
|
|
153
|
+
`--config` pointing at a file the customer supplied through
|
|
154
|
+
`ALP_VELA_CONFIG`. Splitting them here rather than passing one name plus a
|
|
155
|
+
boolean keeps the fail-closed guard above the only place that decides which
|
|
156
|
+
is which: an adapter cannot accidentally pass a vendor name by reading a
|
|
157
|
+
flag the wrong way round, because it is never handed one in the field it
|
|
158
|
+
passes freely."""
|
|
159
|
+
block = soc.get("npu_toolchain")
|
|
160
|
+
vela = block.get("vela") if isinstance(block, dict) else None
|
|
161
|
+
if not isinstance(vela, dict):
|
|
162
|
+
return _VelaProfile(None, None, None, None)
|
|
163
|
+
memory_mode = vela.get("memory_mode") or None
|
|
164
|
+
system_config = vela.get("system_config") or None
|
|
165
|
+
if vela.get("system_config_requires_vendor_config") is not False:
|
|
166
|
+
# The filename rides ONLY on this branch. A spec that declares it needs
|
|
167
|
+
# no vendor config has no vendor file to name, and a contradictory spec
|
|
168
|
+
# (`false` plus a filename) must not have that filename quoted at a
|
|
169
|
+
# customer as something they need -- the refusal clause it feeds calls
|
|
170
|
+
# the file proprietary and undistributed, which would then be advice
|
|
171
|
+
# about a download nobody has to make.
|
|
172
|
+
return _VelaProfile(memory_mode, None, system_config,
|
|
173
|
+
vela.get("vendor_config_filename") or None)
|
|
174
|
+
return _VelaProfile(memory_mode, system_config, None, None)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _npu_backend(npu_type: str, subtype: str) -> str | None:
|
|
178
|
+
if npu_type.startswith("ethos-u"):
|
|
179
|
+
return "ethos_u"
|
|
180
|
+
if "drp" in npu_type or "drp" in subtype: # renesas DRP-AI ("drp-ai" / "ai-mac+drp")
|
|
181
|
+
return "drpai"
|
|
182
|
+
if npu_type.startswith("dx") or "deepx" in npu_type:
|
|
183
|
+
return "deepx_dxm1"
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _soc_targets(soc: dict, silicon_ref: str) -> list[TargetSpec]:
|
|
188
|
+
"""One TargetSpec per mappable NPU in a SoC's npus[] (deduped by the caller)."""
|
|
189
|
+
out: list[TargetSpec] = []
|
|
190
|
+
profile = _vela_profile(soc)
|
|
191
|
+
declares_dram = _soc_declares_dram(soc)
|
|
192
|
+
npus = soc.get("npus", [])
|
|
193
|
+
if not isinstance(npus, list):
|
|
194
|
+
# `npus` is schema-typed `"type": "array"` but unvalidated on read
|
|
195
|
+
# (tan-cli#964 is the read-path schema check; this is the last line
|
|
196
|
+
# of defence, not a substitute). A schema-invalid SoC JSON can hand
|
|
197
|
+
# this key a non-list scalar, which `for npu in npus` would raise
|
|
198
|
+
# `TypeError: ... is not iterable` on. Same "no mappable NPUs"
|
|
199
|
+
# outcome a genuinely-absent `npus` key already produces.
|
|
200
|
+
npus = []
|
|
201
|
+
for npu in npus:
|
|
202
|
+
if not isinstance(npu, dict):
|
|
203
|
+
# One level below the `npus` guard above: an ELEMENT of a
|
|
204
|
+
# schema-valid `npus` array can itself be a non-dict scalar on a
|
|
205
|
+
# schema-invalid SoC JSON (tan-cli#967 review -- the guard below
|
|
206
|
+
# protected `npu.get("type")`'s return value but not the
|
|
207
|
+
# `npu.get` CALL, one line above where the review found it).
|
|
208
|
+
# Skip the element -- the identical "no mappable backend for
|
|
209
|
+
# this entry" outcome an unresolvable `type` already produces
|
|
210
|
+
# two blocks down.
|
|
211
|
+
continue
|
|
212
|
+
# `.get("type", "")`'s default fires only when the key is ABSENT --
|
|
213
|
+
# never when it is present with the wrong type -- so a schema-invalid
|
|
214
|
+
# SoC JSON (unvalidated on read; the real fix is tan-cli#964) can hand
|
|
215
|
+
# a non-string straight into `_npu_backend`'s `.startswith`/`in`
|
|
216
|
+
# calls (tan-cli#965). Guard to the same "" unresolved sentinel the
|
|
217
|
+
# #957 family (cores[] `type`) settled on: an unrecognised/
|
|
218
|
+
# unresolvable type falls back to the identical "no mappable
|
|
219
|
+
# backend, skip this npu" outcome a genuinely-absent `type` key
|
|
220
|
+
# already produces, rather than a fourth behaviour. `.lower()` is
|
|
221
|
+
# deliberately NOT applied here (unlike #965's own snippet): it would
|
|
222
|
+
# newly match e.g. "Ethos-U55" and flip `accel_config` from
|
|
223
|
+
# "Ethos-U55-256" to "ethos-u55-256", which check.py:540-542's
|
|
224
|
+
# `accel_config.startswith(f"ethos-{variant}-")` reads -- exactly the
|
|
225
|
+
# fourth behaviour this guard exists to rule out, not a bug fix.
|
|
226
|
+
raw_type = npu.get("type")
|
|
227
|
+
npu_type = raw_type if isinstance(raw_type, str) else ""
|
|
228
|
+
raw_subtype = npu.get("subtype")
|
|
229
|
+
subtype = raw_subtype if isinstance(raw_subtype, str) else ""
|
|
230
|
+
backend = _npu_backend(npu_type, subtype)
|
|
231
|
+
if backend is None:
|
|
232
|
+
continue
|
|
233
|
+
ethos_u = backend == "ethos_u"
|
|
234
|
+
mac_per_cycle = npu.get("mac_per_cycle")
|
|
235
|
+
if ethos_u and not isinstance(mac_per_cycle, int):
|
|
236
|
+
# `mac_per_cycle` is OPTIONAL in `$defs/npu` -- only `type` plus
|
|
237
|
+
# `anyOf(gops, tops)` are required, so a SCHEMA-VALID SoC JSON
|
|
238
|
+
# (e.g. `{"type": "ethos-u55", "gops": 100}`, no `mac_per_cycle`
|
|
239
|
+
# at all) reaches here with nothing to build `accel_config` from.
|
|
240
|
+
# #964 (read-path schema validation) cannot fix this one: the
|
|
241
|
+
# document already passes the schema. There is no honest partial
|
|
242
|
+
# value to fall back to -- an incomplete string like
|
|
243
|
+
# "ethos-u55-" would be a NEW, un-degradeable shape reaching
|
|
244
|
+
# `check.py`'s `accel_config.startswith(...)` reader, worse than
|
|
245
|
+
# either resolving cleanly or not resolving at all. So this NPU
|
|
246
|
+
# is unmappable for the same reason an unrecognised `type` is:
|
|
247
|
+
# skip it, the identical "no mappable backend for this entry"
|
|
248
|
+
# outcome `backend is None` above already produces (this SoC's
|
|
249
|
+
# OTHER npus[] entries, and the always-present `cpu` target,
|
|
250
|
+
# still resolve).
|
|
251
|
+
continue
|
|
252
|
+
accel = f"{npu_type}-{mac_per_cycle}" if ethos_u else ""
|
|
253
|
+
# The profile is a vela flag, so it rides only on the targets vela
|
|
254
|
+
# compiles: a DRP-AI or DEEPX target on the same die must not carry
|
|
255
|
+
# (and its adapter must never be handed) an Ethos-U memory mode. The
|
|
256
|
+
# DRAM fact is NOT a vela flag -- it is a property of the silicon this
|
|
257
|
+
# target runs on, like `silicon_ref` -- so it rides on every target the
|
|
258
|
+
# spec produces, whether or not any adapter reads it today.
|
|
259
|
+
out.append(TargetSpec(backend=backend, silicon_ref=silicon_ref, accel_config=accel,
|
|
260
|
+
vela_memory_mode=profile.memory_mode if ethos_u else None,
|
|
261
|
+
vela_system_config=profile.system_config if ethos_u else None,
|
|
262
|
+
vela_vendor_system_config=(
|
|
263
|
+
profile.vendor_system_config if ethos_u else None),
|
|
264
|
+
vela_vendor_config_filename=(
|
|
265
|
+
profile.vendor_config_filename if ethos_u else None),
|
|
266
|
+
soc_declares_dram=declares_dram,
|
|
267
|
+
paired_core=npu.get("paired_core") or None))
|
|
268
|
+
return out
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _discrete_socs(sku: str, host_ref: str, metadata_root: Path) -> list[tuple[str, dict]]:
|
|
272
|
+
"""SoCs (other than the host) whose variants[].alp_module_skus list this SKU --
|
|
273
|
+
i.e. on-module discrete accelerators wired to the host (DEEPX DX-M1 on V2M)."""
|
|
274
|
+
found: list[tuple[str, dict]] = []
|
|
275
|
+
for path in sorted((metadata_root / "socs").glob("**/*.json")):
|
|
276
|
+
soc = json.loads(path.read_text(encoding="utf-8"))
|
|
277
|
+
if not isinstance(soc, dict):
|
|
278
|
+
# A SoC JSON that parses but is not an object (e.g. a bare
|
|
279
|
+
# array) -- every read below is `soc.get(...)`/`v.get(...)`.
|
|
280
|
+
# This is an ENUMERATION over every file under `socs/`, the same
|
|
281
|
+
# shape `analyze.py`'s `_load_table` guards for a single lookup
|
|
282
|
+
# ("letting a list or scalar through turns a downstream `.get()`
|
|
283
|
+
# into an uncaught AttributeError"): skip the one bad file, don't
|
|
284
|
+
# abort the sweep over the rest of them.
|
|
285
|
+
continue
|
|
286
|
+
ref = soc.get("ref")
|
|
287
|
+
if not ref or ref == host_ref:
|
|
288
|
+
continue
|
|
289
|
+
variants = soc.get("variants", [])
|
|
290
|
+
if not isinstance(variants, list):
|
|
291
|
+
variants = []
|
|
292
|
+
# `variants[]` elements are schema-typed as objects but unvalidated
|
|
293
|
+
# on read, the same element-type gap `_soc_targets`'s `npus[]` guard
|
|
294
|
+
# covers above (tan-cli#967 review, folded in here rather than filed
|
|
295
|
+
# separately -- same file, same defect) -- a non-dict element must
|
|
296
|
+
# not reach `v.get(...)`.
|
|
297
|
+
skus = {s for v in variants if isinstance(v, dict)
|
|
298
|
+
for s in v.get("alp_module_skus", [])}
|
|
299
|
+
if sku in skus:
|
|
300
|
+
found.append((ref, soc))
|
|
301
|
+
return found
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def resolve_targets(sku: str, *, metadata_root: Path) -> list[TargetSpec]:
|
|
305
|
+
preset_path = metadata_root / "e1m_modules" / f"{sku}.yaml"
|
|
306
|
+
if not preset_path.is_file():
|
|
307
|
+
raise FileNotFoundError(f"no SoM preset for SKU {sku} at {preset_path}")
|
|
308
|
+
import yaml # noqa: PLC0415 -- deferred (tan-cli#810); see sdk_cmd's `_releases_opener`
|
|
309
|
+
|
|
310
|
+
preset = yaml.safe_load(preset_path.read_text(encoding="utf-8"))
|
|
311
|
+
if not isinstance(preset, dict):
|
|
312
|
+
# Mirrors the `host_soc` guard below (tan-cli#964/#965): a preset
|
|
313
|
+
# YAML that parses but is not a mapping (e.g. a bare list or a bare
|
|
314
|
+
# scalar) must not reach the bare `preset["silicon"]` subscript --
|
|
315
|
+
# that raises a raw TypeError ("list indices must be integers or
|
|
316
|
+
# slices, not str" / "string indices must be integers, not 'str'")
|
|
317
|
+
# instead of a curated error a caller can distinguish from any other
|
|
318
|
+
# TypeError in the call stack (tan-cli#1010).
|
|
319
|
+
raise ValueError(
|
|
320
|
+
f"malformed SoM preset at {preset_path}: expected a YAML "
|
|
321
|
+
f"mapping, got {type(preset).__name__}"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# `.get(...)`, not a bare subscript: `preset` is now known to be a
|
|
325
|
+
# mapping (guard above) but `silicon:` is schema-REQUIRED, not merely
|
|
326
|
+
# unvalidated-optional -- a preset that omits it must still fall into
|
|
327
|
+
# the curated `resolve_soc_path()` ValueError path below (a missing key
|
|
328
|
+
# becomes silicon=None, which is not a valid 3-part ref) rather than
|
|
329
|
+
# raising a raw KeyError from this line (tan-cli#1018 review).
|
|
330
|
+
silicon = preset.get("silicon") # host SoC, e.g. "alif:ensemble:e7"
|
|
331
|
+
# resolve_soc_path() returns None where the old inline 3-tuple unpack
|
|
332
|
+
# raised ValueError, so re-raise it here: callers distinguish a
|
|
333
|
+
# malformed ref (ValueError) from a well-formed ref naming a spec that
|
|
334
|
+
# isn't on disk (FileNotFoundError, below), and collapsing the two
|
|
335
|
+
# would be a behaviour change, not a refactor (#1096).
|
|
336
|
+
soc_path = resolve_soc_path(silicon, metadata_root)
|
|
337
|
+
if soc_path is None:
|
|
338
|
+
raise ValueError(
|
|
339
|
+
f"malformed silicon ref {silicon!r} in {preset_path}: expected "
|
|
340
|
+
f"exactly 3 colon-separated parts (<vendor>:<family>:<part>)"
|
|
341
|
+
)
|
|
342
|
+
if not soc_path.is_file():
|
|
343
|
+
raise FileNotFoundError(f"no SoC spec for {silicon} at {soc_path}")
|
|
344
|
+
host_soc = json.loads(soc_path.read_text(encoding="utf-8"))
|
|
345
|
+
if not isinstance(host_soc, dict):
|
|
346
|
+
# Unlike `_discrete_socs`'s enumeration over every socs/*.json below
|
|
347
|
+
# (where one bad file is skipped and the sweep continues), this IS
|
|
348
|
+
# THE ONE host SoC spec `silicon:` names -- there is no "skip it and
|
|
349
|
+
# try the next one". Malformed content at a path that exists is
|
|
350
|
+
# reported the same way this function already reports a malformed
|
|
351
|
+
# ref (above) or a missing path (just above this): raise, don't let
|
|
352
|
+
# it flow into `_soc_targets`'s `.get()` calls as a different,
|
|
353
|
+
# uncaught exception shape.
|
|
354
|
+
raise ValueError(
|
|
355
|
+
f"malformed SoC spec at {soc_path}: expected a JSON object, "
|
|
356
|
+
f"got {type(host_soc).__name__}"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
specs: list[TargetSpec] = []
|
|
360
|
+
seen: set[tuple[str, str]] = set()
|
|
361
|
+
|
|
362
|
+
def _add(spec: TargetSpec) -> None:
|
|
363
|
+
key = (spec.backend, spec.accel_config)
|
|
364
|
+
if key not in seen:
|
|
365
|
+
seen.add(key)
|
|
366
|
+
specs.append(spec)
|
|
367
|
+
|
|
368
|
+
for spec in _soc_targets(host_soc, silicon): # host SoC NPUs
|
|
369
|
+
_add(spec)
|
|
370
|
+
for ref, dsoc in _discrete_socs(sku, silicon, metadata_root): # on-module discrete NPUs
|
|
371
|
+
for spec in _soc_targets(dsoc, ref):
|
|
372
|
+
_add(spec)
|
|
373
|
+
_add(TargetSpec(backend="cpu", silicon_ref="*", accel_config="")) # CPU always present
|
|
374
|
+
return specs
|
tan/model/tensorio.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Extract model tensor + operator descriptors from a TFLite flatbuffer.
|
|
3
|
+
|
|
4
|
+
`extract_io` feeds the `.alpmodel` manifest (Stage-1a `Tensor` records,
|
|
5
|
+
dtype/rank/shape/scale/zp -> mirrors alp_inference_tensor_t). `extract_ops`
|
|
6
|
+
feeds `tan.model.analyze`'s static NPU-eligibility screen (ADR-0028 amendment):
|
|
7
|
+
per-operator name + shape/const-ness + a best-effort MAC estimate, so the
|
|
8
|
+
screen can weight coverage by compute rather than by op count.
|
|
9
|
+
|
|
10
|
+
Both raise `OSError` when the source cannot be read (missing file,
|
|
11
|
+
permission error, ...) -- that is a real failure, never swallowed. Otherwise
|
|
12
|
+
both are best-effort: `[]` (or `[], []`) for a source that IS readable but
|
|
13
|
+
isn't a .tflite, for a missing `tflite` reader (from the `model-io` extra),
|
|
14
|
+
or for bytes that don't parse. The model still packages (or scores as
|
|
15
|
+
`undetermined`), just without this metadata (compile-what's-available)."""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from .manifest import Tensor
|
|
20
|
+
|
|
21
|
+
# Shared TFLite TensorType -> SDK dtype-string mapping. Honest about the gap:
|
|
22
|
+
# an unmapped type (e.g. INT64/BOOL) reads as "tflite:<code>" rather than a
|
|
23
|
+
# silently wrong "f32" -- the SDK Tensor.dtype vocabulary has no such types.
|
|
24
|
+
# Built by reflecting on tflite.TensorType rather than hand-transcribing the
|
|
25
|
+
# numeric codes: the codes are non-contiguous with the SDK dtype vocabulary
|
|
26
|
+
# (six wanted types out of eleven declared), so a hand-maintained literal
|
|
27
|
+
# would need to track schema.fbs by hand for no benefit over reflection.
|
|
28
|
+
_DTYPE_WANTED = {"FLOAT32": "f32", "FLOAT16": "f16", "INT32": "int32",
|
|
29
|
+
"UINT8": "uint8", "INT16": "int16", "INT8": "int8"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def tflite_reader_available() -> bool:
|
|
33
|
+
"""True when the `tflite` reader (the `model-io` extra) is importable on
|
|
34
|
+
this host. `extract_io`/`extract_ops` deliberately swallow a missing
|
|
35
|
+
reader into an empty best-effort result -- the right contract for
|
|
36
|
+
`build_model`'s "compile what's available" stance -- but a caller that
|
|
37
|
+
needs to tell "the reader isn't installed" apart from "this model
|
|
38
|
+
genuinely has no operators" (both look like `[]` from `extract_ops`,
|
|
39
|
+
`tan.model.check`'s MAJOR 2 review) reads this directly instead of
|
|
40
|
+
re-deriving it from an empty list, which the second case returns too."""
|
|
41
|
+
try:
|
|
42
|
+
import tflite # noqa: F401
|
|
43
|
+
except ImportError:
|
|
44
|
+
return False
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _dtype_names(tflite) -> dict[int, str]:
|
|
49
|
+
tt = tflite.TensorType
|
|
50
|
+
return {getattr(tt, k): v for k, v in _DTYPE_WANTED.items()}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _dtype_name(dtype_names: dict[int, str], code: int) -> str:
|
|
54
|
+
return dtype_names.get(code, f"tflite:{code}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def extract_io(source: Path, *, raw: bytes | None = None) -> tuple[list[Tensor], list[Tensor]]:
|
|
58
|
+
# Reuse the caller's already-read bytes when provided (build_model reads the
|
|
59
|
+
# source once for the manifest sha); an OSError on our own read is a real
|
|
60
|
+
# failure, not a parse problem -- read (or take the caller's bytes) BEFORE
|
|
61
|
+
# *any* format-dependent short-circuit below (the suffix check included),
|
|
62
|
+
# so "the source is unreadable" is never masked by "the reader isn't
|
|
63
|
+
# installed" OR by "this isn't a .tflite source". All three would
|
|
64
|
+
# otherwise collapse to the identical `[], []`, which silently hides a
|
|
65
|
+
# real missing/unreadable source -- on a bare-install host (no `model-io`
|
|
66
|
+
# extra) for .tflite, and on EVERY host for .onnx, since ONNX I/O
|
|
67
|
+
# extraction has no reader yet at all.
|
|
68
|
+
if raw is None:
|
|
69
|
+
raw = source.read_bytes()
|
|
70
|
+
if source.suffix.lower() != ".tflite":
|
|
71
|
+
return [], [] # ONNX I/O extraction is a later follow-up
|
|
72
|
+
try:
|
|
73
|
+
import tflite
|
|
74
|
+
except ImportError:
|
|
75
|
+
return [], [] # parser not installed -> skip
|
|
76
|
+
try:
|
|
77
|
+
model = tflite.Model.GetRootAs(raw, 0)
|
|
78
|
+
if model.SubgraphsLength() == 0:
|
|
79
|
+
return [], []
|
|
80
|
+
g = model.Subgraphs(0)
|
|
81
|
+
dtype_names = _dtype_names(tflite)
|
|
82
|
+
ins = [_describe(g, g.Inputs(i), dtype_names) for i in range(g.InputsLength())]
|
|
83
|
+
outs = [_describe(g, g.Outputs(i), dtype_names) for i in range(g.OutputsLength())]
|
|
84
|
+
return ins, outs
|
|
85
|
+
except Exception:
|
|
86
|
+
return [], [] # malformed / unexpected schema -> no metadata
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _describe(g, idx: int, dtype_names: dict[int, str]) -> Tensor:
|
|
90
|
+
ten = g.Tensors(idx)
|
|
91
|
+
shape = [int(ten.Shape(i)) for i in range(ten.ShapeLength())]
|
|
92
|
+
qp = ten.Quantization()
|
|
93
|
+
scale = float(qp.Scale(0)) if qp is not None and qp.ScaleLength() else 0.0
|
|
94
|
+
zp = int(qp.ZeroPoint(0)) if qp is not None and qp.ZeroPointLength() else 0
|
|
95
|
+
return Tensor(dtype=_dtype_name(dtype_names, ten.Type()), rank=len(shape), shape=shape,
|
|
96
|
+
scale=scale, zp=zp)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class TensorDesc:
|
|
101
|
+
"""One operator's input/output tensor, static-shape only (no runtime data)."""
|
|
102
|
+
|
|
103
|
+
shape: list[int]
|
|
104
|
+
dtype: str
|
|
105
|
+
nbytes: int # 0 for a non-const (activation) tensor
|
|
106
|
+
is_const: bool # True when the flatbuffer carries baked-in data (weights/bias)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class OpDesc:
|
|
111
|
+
"""One model operator, as spelled in its own source vocabulary (TFLite
|
|
112
|
+
builtin-op name today; an ONNX op-type string once ONNX extraction lands)."""
|
|
113
|
+
|
|
114
|
+
op: str
|
|
115
|
+
inputs: list[TensorDesc] = field(default_factory=list)
|
|
116
|
+
outputs: list[TensorDesc] = field(default_factory=list)
|
|
117
|
+
macs: int = 0 # best-effort static estimate; 0 when not computable
|
|
118
|
+
# The vocabulary `op` is spelled in -- "tflite" (the only extractor that
|
|
119
|
+
# exists today) or "onnx" once ONNX extraction lands. `tan.model.analyze`
|
|
120
|
+
# compares THIS against a support table's own `op_namespace`, never the
|
|
121
|
+
# caller-supplied `src_format`, so a mislabelled format cannot make two
|
|
122
|
+
# incomparable op vocabularies look like a match. Required kw_only, no
|
|
123
|
+
# default: a silent "tflite" default let a caller build an ONNX-spelled
|
|
124
|
+
# OpDesc that still claimed the TFLite vocabulary, which the namespace
|
|
125
|
+
# guard in analyze.py then trusted -- every construction site (extractor
|
|
126
|
+
# or test) must say which vocabulary it means.
|
|
127
|
+
op_namespace: str = field(kw_only=True)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def extract_ops(source: Path, *, raw: bytes | None = None) -> list[OpDesc]:
|
|
131
|
+
"""Walk a TFLite flatbuffer's first subgraph into `OpDesc`s for the static
|
|
132
|
+
NPU-eligibility screen. Raises `OSError` when @source cannot be read --
|
|
133
|
+
a real failure, surfaced regardless of format. Otherwise best-effort:
|
|
134
|
+
`[]` for a readable non-.tflite source (ONNX extraction is a follow-on),
|
|
135
|
+
a missing `tflite` reader, or unparseable bytes."""
|
|
136
|
+
# Read (or take the caller's bytes) BEFORE any format-dependent
|
|
137
|
+
# short-circuit -- suffix check included -- so an unreadable/missing
|
|
138
|
+
# source raises regardless of extension, rather than a `.onnx` source
|
|
139
|
+
# reading identically to "this model genuinely has no operators".
|
|
140
|
+
if raw is None:
|
|
141
|
+
raw = source.read_bytes()
|
|
142
|
+
if source.suffix.lower() != ".tflite":
|
|
143
|
+
return []
|
|
144
|
+
try:
|
|
145
|
+
import tflite
|
|
146
|
+
except ImportError:
|
|
147
|
+
return []
|
|
148
|
+
try:
|
|
149
|
+
model = tflite.Model.GetRootAs(raw, 0)
|
|
150
|
+
if model.SubgraphsLength() == 0:
|
|
151
|
+
return []
|
|
152
|
+
g = model.Subgraphs(0)
|
|
153
|
+
opcodes = [model.OperatorCodes(i) for i in range(model.OperatorCodesLength())]
|
|
154
|
+
names = _builtin_op_names(tflite)
|
|
155
|
+
dtype_names = _dtype_names(tflite)
|
|
156
|
+
custom_code = tflite.BuiltinOperator.CUSTOM
|
|
157
|
+
ops: list[OpDesc] = []
|
|
158
|
+
for i in range(g.OperatorsLength()):
|
|
159
|
+
op = g.Operators(i)
|
|
160
|
+
oc = opcodes[op.OpcodeIndex()]
|
|
161
|
+
code = oc.BuiltinCode() or oc.DeprecatedBuiltinCode()
|
|
162
|
+
if code == custom_code:
|
|
163
|
+
raw_name = oc.CustomCode()
|
|
164
|
+
name = raw_name.decode("utf-8", "replace") if raw_name else "CUSTOM"
|
|
165
|
+
else:
|
|
166
|
+
name = names.get(code, f"tflite:{code}")
|
|
167
|
+
in_tensors = [_tensor_desc(model, g, op.Inputs(j), dtype_names)
|
|
168
|
+
for j in range(op.InputsLength()) if op.Inputs(j) >= 0]
|
|
169
|
+
# -1 marks an absent optional input (e.g. a bias-less CONV_2D).
|
|
170
|
+
out_tensors = [_tensor_desc(model, g, op.Outputs(j), dtype_names)
|
|
171
|
+
for j in range(op.OutputsLength()) if op.Outputs(j) >= 0]
|
|
172
|
+
ops.append(OpDesc(op=name, inputs=in_tensors, outputs=out_tensors,
|
|
173
|
+
macs=_estimate_macs(name, in_tensors, out_tensors),
|
|
174
|
+
op_namespace="tflite"))
|
|
175
|
+
return ops
|
|
176
|
+
except Exception:
|
|
177
|
+
return [] # malformed / unexpected schema -> no metadata
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _builtin_op_names(tflite) -> dict[int, str]:
|
|
181
|
+
bo = tflite.BuiltinOperator
|
|
182
|
+
return {getattr(bo, n): n for n in dir(bo) if n.isupper() and isinstance(getattr(bo, n), int)}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _tensor_desc(model, g, idx: int, dtype_names: dict[int, str]) -> TensorDesc:
|
|
186
|
+
ten = g.Tensors(idx)
|
|
187
|
+
shape = [int(ten.Shape(i)) for i in range(ten.ShapeLength())]
|
|
188
|
+
buf = model.Buffers(ten.Buffer())
|
|
189
|
+
nbytes = buf.DataLength() if buf is not None else 0
|
|
190
|
+
return TensorDesc(shape=shape, dtype=_dtype_name(dtype_names, ten.Type()), nbytes=nbytes,
|
|
191
|
+
is_const=nbytes > 0)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Positional index of the filter/weights input tensor per TFLite operator
|
|
195
|
+
# kernel (kFilterTensor / kWeightsTensor in the TFLite C++ kernels), keyed by
|
|
196
|
+
# builtin-op name. NOT "the first const input of the expected rank" -- that
|
|
197
|
+
# heuristic silently grabs whichever constant happens to come first once a
|
|
198
|
+
# graph has been const-folded, which shadows the real filter and can inflate
|
|
199
|
+
# the MAC estimate by many times (see test_conv_2d_filter_is_selected_
|
|
200
|
+
# positionally_not_by_first_matching_const). Every op below carries a
|
|
201
|
+
# MANDATORY (never optional) tensor at index 0 -- the activation for
|
|
202
|
+
# FULLY_CONNECTED/CONV_2D/DEPTHWISE_CONV_2D, kOutputShapeTensor for
|
|
203
|
+
# TRANSPOSE_CONV -- so the filter index below never shifts under an
|
|
204
|
+
# absent-optional-input reindex; only the trailing bias tensor is ever
|
|
205
|
+
# optional (see _ACTIVATION_INPUT_INDEX below for TRANSPOSE_CONV's one
|
|
206
|
+
# further wrinkle: its activation isn't at index 0 the way it is here).
|
|
207
|
+
_FILTER_INPUT_INDEX = {
|
|
208
|
+
"FULLY_CONNECTED": 1, # kInputTensor=0, kWeightsTensor=1, kBiasTensor=2 (optional)
|
|
209
|
+
"CONV_2D": 1, # kInputTensor=0, kFilterTensor=1, kBiasTensor=2 (optional)
|
|
210
|
+
"DEPTHWISE_CONV_2D": 1, # kInputTensor=0, kFilterTensor=1, kBiasTensor=2 (optional)
|
|
211
|
+
"TRANSPOSE_CONV": 1, # kOutputShapeTensor=0, kWeightsTensor=1, kDataInputTensor=2, kBiasTensor=3 (optional)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# TRANSPOSE_CONV alone needs the INPUT activation positionally too (its
|
|
215
|
+
# filter is applied per input position, not per output position -- see the
|
|
216
|
+
# formula below), and that activation is not at index 0 the way it is for
|
|
217
|
+
# every other op here.
|
|
218
|
+
_ACTIVATION_INPUT_INDEX = {
|
|
219
|
+
"TRANSPOSE_CONV": 2, # kDataInputTensor
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _filter_weight(op_name: str, inputs: list[TensorDesc], rank: int) -> TensorDesc | None:
|
|
224
|
+
idx = _FILTER_INPUT_INDEX.get(op_name)
|
|
225
|
+
if idx is None or idx >= len(inputs):
|
|
226
|
+
return None
|
|
227
|
+
w = inputs[idx]
|
|
228
|
+
return w if w.is_const and len(w.shape) == rank else None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _elem_count(shape: list[int]) -> int:
|
|
232
|
+
n = 1
|
|
233
|
+
for d in shape:
|
|
234
|
+
n *= max(int(d), 0)
|
|
235
|
+
return n
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _estimate_macs(op_name: str, inputs: list[TensorDesc], outputs: list[TensorDesc]) -> int:
|
|
239
|
+
"""Best-effort MAC count from static shapes for the compute-dominant op
|
|
240
|
+
kinds (conv/dense); 0 for everything else. MAC weighting exists to stop
|
|
241
|
+
op-count coverage from hiding a conv backbone behind a wall of cheap
|
|
242
|
+
elementwise ops, not to model every op's cost exactly -- an upper-bound
|
|
243
|
+
screening heuristic, same stance as the rest of this module."""
|
|
244
|
+
if not outputs:
|
|
245
|
+
return 0
|
|
246
|
+
|
|
247
|
+
if op_name == "TRANSPOSE_CONV":
|
|
248
|
+
# Unlike CONV_2D, the filter slides over the INPUT (the op upsamples:
|
|
249
|
+
# output is stride-larger than input), so costing off the output
|
|
250
|
+
# element count -- as CONV_2D correctly does -- over-counts by
|
|
251
|
+
# stride^2. Cost off the actual input-activation tensor's element
|
|
252
|
+
# count instead -- this branch never reads out_elems, so it must sit
|
|
253
|
+
# ahead of (not behind) the out_elems==0 guard below: an output shape
|
|
254
|
+
# that happens to compute to 0 elements (e.g. a dynamic dim) must not
|
|
255
|
+
# zero out a fully-computable in_elems * kh * kw * out_c estimate.
|
|
256
|
+
w = _filter_weight(op_name, inputs, 4) # [out_c, kh, kw, in_c] (OHWI)
|
|
257
|
+
if w is None:
|
|
258
|
+
return 0
|
|
259
|
+
out_c, kh, kw, _ = w.shape
|
|
260
|
+
act_idx = _ACTIVATION_INPUT_INDEX[op_name]
|
|
261
|
+
if act_idx >= len(inputs):
|
|
262
|
+
return 0
|
|
263
|
+
in_elems = _elem_count(inputs[act_idx].shape)
|
|
264
|
+
if in_elems == 0:
|
|
265
|
+
return 0
|
|
266
|
+
return in_elems * kh * kw * out_c
|
|
267
|
+
|
|
268
|
+
out_elems = _elem_count(outputs[0].shape)
|
|
269
|
+
if out_elems == 0:
|
|
270
|
+
return 0
|
|
271
|
+
|
|
272
|
+
if op_name == "FULLY_CONNECTED":
|
|
273
|
+
w = _filter_weight(op_name, inputs, 2) # [out_features, in_features]
|
|
274
|
+
return out_elems * w.shape[-1] if w else 0
|
|
275
|
+
if op_name == "CONV_2D":
|
|
276
|
+
w = _filter_weight(op_name, inputs, 4) # [out_c, kh, kw, in_c] (OHWI)
|
|
277
|
+
if w is None:
|
|
278
|
+
return 0
|
|
279
|
+
_, kh, kw, in_c = w.shape
|
|
280
|
+
return out_elems * kh * kw * in_c
|
|
281
|
+
if op_name == "DEPTHWISE_CONV_2D":
|
|
282
|
+
w = _filter_weight(op_name, inputs, 4) # [1, kh, kw, out_c]
|
|
283
|
+
if w is None:
|
|
284
|
+
return 0
|
|
285
|
+
_, kh, kw, _ = w.shape
|
|
286
|
+
return out_elems * kh * kw
|
|
287
|
+
return 0
|