From 1d48089c652421cd070b06f42c01027bbdf6ead0 Mon Sep 17 00:00:00 2001 From: Erik Lundell Date: Tue, 7 Apr 2026 16:21:43 +0200 Subject: [PATCH] Arm backend: Add tools for finding pte info - Add devtools/pte_tool/pte_info.py for getting all compile specs from a pte - Add backends/arm/scripts/ethosu_pte_info.py for Ethos-U specific info. - Use ethosu_pte_info.py in build_executor_runner to set default parameters. Signed-off-by: Erik Lundell Change-Id: I7ce38fb7b103b628ea69730e9380dd628b44caf3 --- backends/arm/scripts/build_executor_runner.sh | 42 ++++- backends/arm/scripts/ethosu_pte_info.py | 143 +++++++++++++++++ .../arm/test/misc/test_ethosu_pte_info.py | 131 +++++++++++++++ devtools/pte_tool/BUCK | 25 +++ devtools/pte_tool/pte_info.py | 149 ++++++++++++++++++ devtools/pte_tool/tests/TARGETS | 13 ++ devtools/pte_tool/tests/test_pte_info.py | 114 ++++++++++++++ 7 files changed, 613 insertions(+), 4 deletions(-) create mode 100644 backends/arm/scripts/ethosu_pte_info.py create mode 100644 backends/arm/test/misc/test_ethosu_pte_info.py create mode 100644 devtools/pte_tool/pte_info.py create mode 100644 devtools/pte_tool/tests/test_pte_info.py diff --git a/backends/arm/scripts/build_executor_runner.sh b/backends/arm/scripts/build_executor_runner.sh index f2ffd2e27a7..4efcbd85bf1 100755 --- a/backends/arm/scripts/build_executor_runner.sh +++ b/backends/arm/scripts/build_executor_runner.sh @@ -15,7 +15,7 @@ _setup_msg="please refer to ${et_root_dir}/examples/arm/setup.sh to properly ins source "${script_dir}/utils.sh" pte_file="" -target="ethos-u55-128" +target="" build_type="Release" bundleio=false system_config="" @@ -37,14 +37,15 @@ help() { echo " --pte=||semihosting Set to a pte file (generated by the aot_arm_compier) to include the model in the elf." echo " Or a hex address in the format of 0x00000000 if placed in memory you need to place it on this ADDR on your target, with your flash tool or other means." echo " Or specify the word 'semihosting' to supply pte at runtime." - echo " --target= Target to build and run for Default: ${target}" + echo " --target= Target to build and run for. Defaults to the PTE Ethos-U compile spec target when available, otherwise ethos-u55-128" echo " --build_type= Build with Release, Debug or RelWithDebInfo, default is ${build_type}" echo " --bundleio Support both pte and Bundle IO bpte using Devtools BundelIO with Input/RefOutput included" - echo " --system_config= System configuration to select from the Vela configuration file (see vela.ini). Default: Ethos_U55_High_End_Embedded for EthosU55 targets, Ethos_U85_SYS_DRAM_Mid for EthosU85 targets." + echo " Defaults to ON when --pte points to a .bpte file." + echo " --system_config= System configuration to select from the Vela configuration file (see vela.ini). Defaults to the Ethos-U delegate compile spec in the PTE when available, otherwise target-specific defaults." echo " NOTE: If given, this option must match the given target. This option along with the memory_mode sets timing adapter values customized for specific hardware, see ./executor_runner/CMakeLists.txt." echo " --memory_mode= Vela memory mode, used for setting the Timing Adapter parameters of the Corstone platforms." echo " Valid values are Shared_Sram(for Ethos-U55, Ethos-U65, Ethos-85), Sram_Only(for Ethos-U55, Ethos-U65, Ethos-U85) or Dedicated_Sram(for Ethos-U65, Ethos-U85)." - echo " Default: Shared_Sram for the Ethos-U55 and Sram_Only for the Ethos-U85" + echo " Defaults to the Ethos-U delegate compile spec in the PTE when available, otherwise target-specific defaults." echo " --etdump Adds Devtools etdump support to track timing and output, etdump area will be base64 encoded in the log" echo " --extra_build_flags= Extra flags to pass to cmake like -DET_ARM_BAREMETAL_METHOD_ALLOCATOR_POOL_SIZE=60000 Default: none " echo " --output= Output folder Default: /_.pte" @@ -114,6 +115,9 @@ else else echo "PTE included in elf from file ${pte_file}" pte_file=$(realpath ${pte_file}) + if [[ ${pte_file} == *.bpte ]]; then + bundleio=true + fi pte_data="-DET_PTE_FILE_PATH:PATH=${pte_file}" if [ "$output_folder_set" = false ] ; then # remove file ending @@ -130,6 +134,36 @@ et_build_dir=${et_build_root}/cmake-out mkdir -p ${et_build_dir} et_build_dir=$(realpath ${et_build_dir}) +# If a PTE file is supplied, derive any unset Ethos-U runtime config from it. +if [[ -f "${pte_file}" ]] && [[ -z "${target}" || -z "${system_config}" || -z "${memory_mode}" ]]; then + set +e + extracted_config=$(python3 "${script_dir}/ethosu_pte_info.py" --format=tsv "${pte_file}") + extracted_status=$? + set -e + + if [[ ${extracted_status} -eq 0 ]]; then + IFS=$'\t' read -r extracted_target extracted_system_config extracted_memory_mode <<< "${extracted_config}" + + if [[ ${target} == "" ]]; then + target="${extracted_target}" + fi + if [[ ${system_config} == "" ]]; then + system_config="${extracted_system_config}" + fi + if [[ ${memory_mode} == "" ]]; then + memory_mode="${extracted_memory_mode}" + fi + else + echo "Warning: Failed to derive Ethos-U compile spec defaults from ${pte_file}" + fi +fi + +# Default values if getting defaults from compile spec failed. +if [[ ${target} == "" ]] +then + target="ethos-u55-128" +fi + if [[ ${system_config} == "" ]] then system_config="Ethos_U55_High_End_Embedded" diff --git a/backends/arm/scripts/ethosu_pte_info.py b/backends/arm/scripts/ethosu_pte_info.py new file mode 100644 index 00000000000..262e9e823c3 --- /dev/null +++ b/backends/arm/scripts/ethosu_pte_info.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 + +# Copyright 2026 Arm Limited and/or its affiliates. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from __future__ import annotations + +import argparse +import json +from dataclasses import asdict, dataclass +from pathlib import Path + +from executorch.backends.arm.ethosu import EthosUBackend, EthosUCompileSpec +from executorch.devtools.pte_tool.pte_info import ( + DelegateInfo, + get_delegate_infos_from_pte, +) + + +@dataclass(frozen=True) +class EthosUDelegateConfig: + target: str + system_config: str + memory_mode: str + + +def _extract_flag_value(flags: list[str], prefix: str) -> str | None: + for flag in flags: + if flag.startswith(prefix): + return flag.removeprefix(prefix) + return None + + +def _config_from_delegate(delegate_info: DelegateInfo) -> EthosUDelegateConfig: + compile_spec = EthosUCompileSpec._from_list(delegate_info.compile_specs) + if compile_spec.target is None: + raise ValueError("Missing Ethos-U target in delegate compile spec.") + + system_config = _extract_flag_value(compile_spec.compiler_flags, "--system-config=") + if system_config is None: + raise ValueError( + f"Missing --system-config flag in Ethos-U compile spec for {compile_spec.target}." + ) + + memory_mode = _extract_flag_value(compile_spec.compiler_flags, "--memory-mode=") + if memory_mode is None: + raise ValueError( + f"Missing --memory-mode flag in Ethos-U compile spec for {compile_spec.target}." + ) + + return EthosUDelegateConfig( + target=compile_spec.target, + system_config=system_config, + memory_mode=memory_mode, + ) + + +def get_ethosu_delegate_configs_from_pte( + pte_path: str | Path, +) -> list[EthosUDelegateConfig]: + configs: list[EthosUDelegateConfig] = [] + for delegate_info in get_delegate_infos_from_pte(pte_path): + if delegate_info.delegate_id != EthosUBackend.__name__: + continue + configs.append(_config_from_delegate(delegate_info)) + return configs + + +def get_ethosu_delegate_config_from_pte( + pte_path: str | Path, +) -> EthosUDelegateConfig | None: + configs = get_ethosu_delegate_configs_from_pte(pte_path) + if not configs: + return None + + unique_configs = sorted( + { + (config.target, config.system_config, config.memory_mode) + for config in configs + } + ) + if len(unique_configs) != 1: + joined = ", ".join( + f"target={target} system_config={system_config} memory_mode={memory_mode}" + for target, system_config, memory_mode in unique_configs + ) + raise ValueError( + "Found multiple Ethos-U delegate compile spec configurations in " + f"{pte_path}: {joined}" + ) + + target, system_config, memory_mode = unique_configs[0] + return EthosUDelegateConfig( + target=target, + system_config=system_config, + memory_mode=memory_mode, + ) + + +def _get_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Read the Ethos-U delegate compile spec from a .pte/.bpte file and " + "print the resolved target, system_config and memory_mode." + ) + ) + parser.add_argument("pte_path", help="Path to a .pte or .bpte file.") + parser.add_argument( + "--format", + choices=("pretty", "json", "tsv"), + default="pretty", + help="Output format. Default: %(default)s.", + ) + return parser.parse_args() + + +def _print_config(config: EthosUDelegateConfig, output_format: str) -> None: + if output_format == "json": + print(json.dumps(asdict(config), sort_keys=True)) + return + if output_format == "tsv": + print(f"{config.target}\t{config.system_config}\t{config.memory_mode}") + return + + print(f"target={config.target}") + print(f"system_config={config.system_config}") + print(f"memory_mode={config.memory_mode}") + + +def main() -> int: + args = _get_args() + config = get_ethosu_delegate_config_from_pte(args.pte_path) + if config is None: + return 2 + + _print_config(config, args.format) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backends/arm/test/misc/test_ethosu_pte_info.py b/backends/arm/test/misc/test_ethosu_pte_info.py new file mode 100644 index 00000000000..ea4df829393 --- /dev/null +++ b/backends/arm/test/misc/test_ethosu_pte_info.py @@ -0,0 +1,131 @@ +# Copyright 2026 Arm Limited and/or its affiliates. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from executorch.backends.arm.ethosu import EthosUCompileSpec +from executorch.backends.arm.scripts.ethosu_pte_info import ( + get_ethosu_delegate_config_from_pte, +) +from executorch.exir._serialize import _PTEFile, _serialize_pte_binary +from executorch.exir.backend.compile_spec_schema import CompileSpec +from executorch.exir.schema import ( + BackendDelegate, + BackendDelegateDataReference, + BackendDelegateInlineData, + Buffer, + Chain, + ContainerMetadata, + DataLocation, + ExecutionPlan, + Program, + SubsegmentOffsets, +) +from pytest import raises + + +def _make_pte_bytes(*, delegates: list[BackendDelegate]) -> bytes: + program = Program( + version=0, + execution_plan=[ + ExecutionPlan( + name="forward", + container_meta_type=ContainerMetadata( + encoded_inp_str="[]", encoded_out_str="[]" + ), + values=[], + inputs=[], + outputs=[], + chains=[Chain(inputs=[], outputs=[], instructions=[], stacktrace=None)], + operators=[], + delegates=delegates, + non_const_buffer_sizes=[0], + ) + ], + constant_buffer=[Buffer(storage=b"")], + backend_delegate_data=[BackendDelegateInlineData(data=b"delegate-data")], + segments=[], + constant_segment=SubsegmentOffsets(segment_index=0, offsets=[]), + mutable_data_segments=[], + named_data=[], + ) + return bytes(_serialize_pte_binary(_PTEFile(program))) + + +def _write_pte(tmp_path, *delegates: BackendDelegate): + pte_path = tmp_path / "model.pte" + pte_path.write_bytes(_make_pte_bytes(delegates=list(delegates))) + return pte_path + + +def _ethosu_delegate( + target: str, + system_config: str, + memory_mode: str, + index: int = 0, +) -> BackendDelegate: + return BackendDelegate( + id="EthosUBackend", + processed=BackendDelegateDataReference( + location=DataLocation.INLINE, index=index + ), + compile_specs=EthosUCompileSpec( + target=target, + system_config=system_config, + memory_mode=memory_mode, + )._to_list(), + ) + + +def test_ethosu_pte_info_no_target(tmp_path) -> None: + pte_path = _write_pte( + tmp_path, + _ethosu_delegate( + target="ethos-u85-256", + system_config="Ethos_U85_SYS_DRAM_Mid", + memory_mode="Dedicated_Sram_384KB", + ), + ) + + config = get_ethosu_delegate_config_from_pte(pte_path) + + assert config is not None + assert config.target == "ethos-u85-256" + assert config.system_config == "Ethos_U85_SYS_DRAM_Mid" + assert config.memory_mode == "Dedicated_Sram_384KB" + + +def test_ethosu_pte_info_returns_none_without_ethosu_delegate_no_target( + tmp_path, +) -> None: + pte_path = _write_pte( + tmp_path, + BackendDelegate( + id="OtherBackend", + processed=BackendDelegateDataReference( + location=DataLocation.INLINE, index=0 + ), + compile_specs=[CompileSpec(key="k", value=b"v")], + ), + ) + + assert get_ethosu_delegate_config_from_pte(pte_path) is None + + +def test_ethosu_pte_info_rejects_mixed_configs_no_target(tmp_path) -> None: + pte_path = _write_pte( + tmp_path, + _ethosu_delegate( + target="ethos-u55-128", + system_config="Ethos_U55_High_End_Embedded", + memory_mode="Shared_Sram", + ), + _ethosu_delegate( + target="ethos-u85-256", + system_config="Ethos_U85_SYS_DRAM_Mid", + memory_mode="Sram_Only", + ), + ) + + with raises(ValueError, match="multiple Ethos-U delegate compile spec"): + get_ethosu_delegate_config_from_pte(pte_path) diff --git a/devtools/pte_tool/BUCK b/devtools/pte_tool/BUCK index 0cb2d8ab9ec..7730dfa67a7 100644 --- a/devtools/pte_tool/BUCK +++ b/devtools/pte_tool/BUCK @@ -15,6 +15,18 @@ fbcode_target(_kind = runtime.python_library, ], ) +fbcode_target(_kind = runtime.python_library, + name = "pte_info_lib", + srcs = [ + "pte_info.py", + ], + deps = [ + "//executorch/devtools/bundled_program/serialize:lib", + "//executorch/exir/_serialize:lib", + "//executorch/exir/backend:compile_spec_schema", + ], +) + fbcode_target(_kind = runtime.python_binary, name = "diff_pte", srcs = [ @@ -27,3 +39,16 @@ fbcode_target(_kind = runtime.python_binary, "//executorch/exir/_serialize:lib", ], ) + +fbcode_target(_kind = runtime.python_binary, + name = "pte_info", + srcs = [ + "pte_info.py", + ], + main_function = "executorch.devtools.pte_tool.pte_info.main", + deps = [ + "//executorch/devtools/bundled_program/serialize:lib", + "//executorch/exir/_serialize:lib", + "//executorch/exir/backend:compile_spec_schema", + ], +) diff --git a/devtools/pte_tool/pte_info.py b/devtools/pte_tool/pte_info.py new file mode 100644 index 00000000000..3c872b86c20 --- /dev/null +++ b/devtools/pte_tool/pte_info.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 + +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# pyre-unsafe + +from __future__ import annotations + +import argparse +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any, List + +from executorch.devtools.bundled_program.serialize import ( + deserialize_from_flatbuffer_to_bundled_program, +) +from executorch.exir._serialize._program import deserialize_pte_binary +from executorch.exir.backend.compile_spec_schema import CompileSpec + + +@dataclass(frozen=True) +class DelegateInfo: + plan_index: int + plan_name: str + delegate_index: int + delegate_id: str + compile_specs: List[CompileSpec] + + +def _extract_pte_from_bundle(pte_data: bytes) -> bytes: + try: + bundled = deserialize_from_flatbuffer_to_bundled_program(pte_data) + except Exception: + return pte_data + return bundled.program if bundled.program else pte_data + + +def _decode_compile_spec_value(value: bytes) -> str | None: + try: + decoded = value.decode("utf-8") + except UnicodeDecodeError: + return None + + if all(char.isprintable() or char in "\r\n\t" for char in decoded): + return decoded + return None + + +def _compile_spec_to_dict(spec: CompileSpec) -> dict[str, Any]: + value_text = _decode_compile_spec_value(spec.value) + return { + "key": spec.key, + "value_text": value_text, + "value_hex": spec.value.hex(), + } + + +def delegate_info_to_dict(delegate_info: DelegateInfo) -> dict[str, Any]: + return { + "plan_index": delegate_info.plan_index, + "plan_name": delegate_info.plan_name, + "delegate_index": delegate_info.delegate_index, + "delegate_id": delegate_info.delegate_id, + "compile_specs": [ + _compile_spec_to_dict(spec) for spec in delegate_info.compile_specs + ], + } + + +def get_delegate_infos_from_pte(pte_path: str | Path) -> list[DelegateInfo]: + pte_path = Path(pte_path) + pte_data = pte_path.read_bytes() + if pte_path.suffix.lower() == ".bpte": + pte_data = _extract_pte_from_bundle(pte_data) + program = deserialize_pte_binary(pte_data).program + + delegate_infos: list[DelegateInfo] = [] + for plan_index, plan in enumerate(program.execution_plan): + for delegate_index, delegate in enumerate(plan.delegates): + delegate_infos.append( + DelegateInfo( + plan_index=plan_index, + plan_name=plan.name, + delegate_index=delegate_index, + delegate_id=delegate.id, + compile_specs=list(delegate.compile_specs), + ) + ) + return delegate_infos + + +def format_delegate_infos( + delegate_infos: list[DelegateInfo], output_format: str = "pretty" +) -> str: + if output_format == "json": + return json.dumps( + [delegate_info_to_dict(delegate_info) for delegate_info in delegate_infos], + indent=2, + sort_keys=True, + ) + + lines = [] + for delegate_info in delegate_infos: + lines.append( + f"plan {delegate_info.plan_index} {delegate_info.plan_name}, " + f"delegate {delegate_info.delegate_index} {delegate_info.delegate_id}:" + ) + for spec in delegate_info.compile_specs: + spec_dict = _compile_spec_to_dict(spec) + rendered_value = ( + json.dumps(spec_dict["value_text"]) + if spec_dict["value_text"] is not None + else f"0x{spec_dict['value_hex']}" + ) + lines.append(f" {spec_dict['key']}={rendered_value}") + return "\n".join(lines) + + +def _get_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Print all delegate compile_specs from a .pte or .bpte file." + ) + parser.add_argument("pte_path", help="Path to a .pte or .bpte file.") + parser.add_argument( + "--format", + choices=("pretty", "json"), + default="pretty", + help="Output format. Default: %(default)s.", + ) + return parser.parse_args() + + +def main() -> int: + args = _get_args() + delegate_infos = get_delegate_infos_from_pte(args.pte_path) + if not delegate_infos: + return 2 + + print(format_delegate_infos(delegate_infos, args.format)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/devtools/pte_tool/tests/TARGETS b/devtools/pte_tool/tests/TARGETS index f19feb1bacf..1047712e090 100644 --- a/devtools/pte_tool/tests/TARGETS +++ b/devtools/pte_tool/tests/TARGETS @@ -13,3 +13,16 @@ python_unittest( "//executorch/exir/_serialize:lib", ], ) + +python_unittest( + name = "test_pte_info", + srcs = [ + "test_pte_info.py", + ], + deps = [ + "//executorch/devtools/pte_tool:pte_info_lib", + "//executorch/exir/backend:compile_spec_schema", + "//executorch/exir:schema", + "//executorch/exir/_serialize:lib", + ], +) diff --git a/devtools/pte_tool/tests/test_pte_info.py b/devtools/pte_tool/tests/test_pte_info.py new file mode 100644 index 00000000000..20d23e76655 --- /dev/null +++ b/devtools/pte_tool/tests/test_pte_info.py @@ -0,0 +1,114 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# pyre-unsafe + +import json +import tempfile +import unittest + +from executorch.devtools.pte_tool.pte_info import ( + format_delegate_infos, + get_delegate_infos_from_pte, +) +from executorch.exir._serialize._program import PTEFile, serialize_pte_binary +from executorch.exir.backend.compile_spec_schema import CompileSpec +from executorch.exir.schema import ( + BackendDelegate, + BackendDelegateDataReference, + BackendDelegateInlineData, + Buffer, + Chain, + ContainerMetadata, + DataLocation, + ExecutionPlan, + Program, + SubsegmentOffsets, +) + + +def _make_program() -> Program: + return Program( + version=0, + execution_plan=[ + ExecutionPlan( + name="forward", + container_meta_type=ContainerMetadata( + encoded_inp_str="[]", encoded_out_str="[]" + ), + values=[], + inputs=[], + outputs=[], + chains=[Chain(inputs=[], outputs=[], instructions=[], stacktrace=None)], + operators=[], + delegates=[ + BackendDelegate( + id="BackendA", + processed=BackendDelegateDataReference( + location=DataLocation.INLINE, index=0 + ), + compile_specs=[ + CompileSpec(key="mode", value=b"fast"), + CompileSpec(key="binary", value=bytes([0, 255])), + ], + ), + BackendDelegate( + id="BackendB", + processed=BackendDelegateDataReference( + location=DataLocation.INLINE, index=0 + ), + compile_specs=[CompileSpec(key="config", value=b"small")], + ), + ], + non_const_buffer_sizes=[0], + ) + ], + constant_buffer=[Buffer(storage=b"")], + backend_delegate_data=[BackendDelegateInlineData(data=b"delegate-data")], + segments=[], + constant_segment=SubsegmentOffsets(segment_index=0, offsets=[]), + mutable_data_segments=[], + named_data=[], + ) + + +class PteInfoTest(unittest.TestCase): + def test_get_delegate_infos_from_pte(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + pte_path = f"{tmpdir}/model.pte" + with open(pte_path, "wb") as pte_file: + pte_file.write(bytes(serialize_pte_binary(PTEFile(_make_program())))) + + delegate_infos = get_delegate_infos_from_pte(pte_path) + + self.assertEqual(len(delegate_infos), 2) + self.assertEqual(delegate_infos[0].plan_index, 0) + self.assertEqual(delegate_infos[0].plan_name, "forward") + self.assertEqual(delegate_infos[0].delegate_index, 0) + self.assertEqual(delegate_infos[0].delegate_id, "BackendA") + self.assertEqual(delegate_infos[0].compile_specs[0].key, "mode") + self.assertEqual(delegate_infos[0].compile_specs[0].value, b"fast") + self.assertEqual(delegate_infos[1].delegate_id, "BackendB") + + def test_format_delegate_infos(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + pte_path = f"{tmpdir}/model.pte" + with open(pte_path, "wb") as pte_file: + pte_file.write(bytes(serialize_pte_binary(PTEFile(_make_program())))) + + delegate_infos = get_delegate_infos_from_pte(pte_path) + + pretty_output = format_delegate_infos(delegate_infos) + self.assertIn("plan 0 forward, delegate 0 BackendA:", pretty_output) + self.assertIn(' mode="fast"', pretty_output) + self.assertIn(" binary=0x00ff", pretty_output) + + json_output = json.loads( + format_delegate_infos(delegate_infos, output_format="json") + ) + self.assertEqual(json_output[0]["delegate_id"], "BackendA") + self.assertEqual(json_output[0]["compile_specs"][0]["value_text"], "fast") + self.assertEqual(json_output[0]["compile_specs"][1]["value_hex"], "00ff")