diff --git a/backends/arm/BUCK b/backends/arm/BUCK index 95092effd7c..4efbdf6c69b 100644 --- a/backends/arm/BUCK +++ b/backends/arm/BUCK @@ -84,6 +84,7 @@ fbcode_target( "ethosu/__init__.py", "ethosu/backend.py", "ethosu/compile_spec.py", + "ethosu/parse_delegate_metadata.py", "ethosu/partitioner.py", ], deps = [ diff --git a/backends/arm/CMakeLists.txt b/backends/arm/CMakeLists.txt index 69ec45ccd17..15371937dd4 100644 --- a/backends/arm/CMakeLists.txt +++ b/backends/arm/CMakeLists.txt @@ -100,7 +100,9 @@ if(EXECUTORCH_BUILD_ARM_BAREMETAL OR EXECUTORCH_BUILD_ARM_ETHOSU_LINUX) target_include_directories( ethosu_execution_stats PRIVATE ${DRIVER_ETHOSU_INCLUDE_DIR} ) - target_link_libraries(ethosu_execution_stats PUBLIC ethosu_core_driver) + target_link_libraries( + ethosu_execution_stats PUBLIC ethosu_core_driver executorch_core + ) elseif(EXECUTORCH_BUILD_ARM_ETHOSU_LINUX) target_sources( executorch_delegate_ethos_u diff --git a/backends/arm/ethosu/parse_delegate_metadata.py b/backends/arm/ethosu/parse_delegate_metadata.py new file mode 100644 index 00000000000..100e6981031 --- /dev/null +++ b/backends/arm/ethosu/parse_delegate_metadata.py @@ -0,0 +1,441 @@ +# 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. +"""Decode experimental Ethos-U PMU metadata from ETDump profile events. + +The wire format and public Python types may change without compatibility +support. + +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import IntEnum +from struct import Struct +from typing import Any, Sequence + +_HEADER = Struct("<4sIIIIQQI") +_COUNTER = Struct(" EthosUPmuProfileEvent: + """Decode one Ethos-U PMU delegate metadata blob (experimental API). + + Args: + metadata (bytes): Raw ETDump delegate debug metadata. + + Returns: + EthosUPmuProfileEvent: Decoded NPU cycles and PMU counters. + + Raises: + ValueError: If the blob has an invalid format, version, or event ID. + + """ + if len(metadata) < 4: + raise ValueError("Ethos-U PMU metadata is shorter than its header") + if metadata[:3] != _MAGIC_PREFIX: + raise ValueError(f"Unsupported Ethos-U PMU metadata magic: {metadata[:4]!r}") + try: + version = int(metadata[3:4]) + except ValueError as exc: + raise ValueError("Invalid Ethos-U PMU metadata version") from exc + parsers = {1: _parse_pmu_profile_event_v1} + parser = parsers.get(version) + if parser is None: + raise ValueError( + f"Unsupported Ethos-U PMU metadata version: {version}; " + f"supported versions: {set(parsers.keys())}" + ) + else: + return parser(metadata) + + +def _parse_pmu_profile_event_v1(metadata: bytes) -> EthosUPmuProfileEvent: + if len(metadata) < _HEADER.size: + raise ValueError("Ethos-U PMU metadata is shorter than its header") + + ( + _, + product, + macs_per_cycle, + platform_id, + flags, + register_base, + cycle_count, + counter_count, + ) = _HEADER.unpack_from(metadata) + try: + platform = EthosUPlatform(platform_id) + except ValueError as exc: + raise ValueError(f"Unknown Ethos-U platform ID: {platform_id}") from exc + try: + product_type = EthosUProduct(product) + except ValueError as exc: + raise ValueError(f"Unknown Ethos-U product ID: {product}") from exc + if flags & ~0b11: + raise ValueError(f"Unsupported Ethos-U PMU flags: {flags:#x}") + if counter_count > _MAX_COUNTERS: + raise ValueError(f"Too many Ethos-U PMU counters: {counter_count}") + + expected_size = _HEADER.size + counter_count * _COUNTER.size + if len(metadata) != expected_size: + raise ValueError( + f"Ethos-U PMU metadata has {len(metadata)} bytes; expected {expected_size}" + ) + + counters = _parse_counters(metadata, _HEADER.size, counter_count) + + return EthosUPmuProfileEvent( + product=product_type, + platform=platform, + macs_per_cycle=macs_per_cycle, + success=bool(flags & 2), + register_base=register_base, + cycle_count=float("nan") if flags & 1 else float(cycle_count), + cycle_overflow=bool(flags & 1), + counters=counters, + ) + + +def _parse_counters( + metadata: bytes, offset: int, counter_count: int +) -> tuple[EthosUPmuCounter, ...]: + counters = [] + for slot in range(counter_count): + event_id, count, overflow = _COUNTER.unpack_from( + metadata, offset + slot * _COUNTER.size + ) + try: + event_type = EthosUPmuEvent(event_id) + except ValueError as exc: + raise ValueError(f"Unknown Ethos-U PMU event ID: {event_id}") from exc + if overflow not in (0, 1): + raise ValueError(f"Invalid Ethos-U PMU overflow value: {overflow}") + counters.append( + EthosUPmuCounter( + event_type, float("nan") if overflow else float(count), bool(overflow) + ) + ) + return tuple(counters) + + +def parse_ethosu_delegate_pmu_metadata( + metadata_list: Sequence[bytes | bytearray | str | None], +) -> dict[str, Any]: + """Decode Ethos-U PMU samples for Inspector (experimental API). + + Args: + metadata_list (Sequence[bytes | bytearray | str | None]): Raw samples. + + Returns: + dict[str, Any]: Decoded samples in the `ethosu_pmu` key, or an empty + dictionary for unrelated delegate metadata. Missing samples retain + their positions as None. + + """ + records: list[dict[str, Any] | None] = [] + found = False + for metadata in metadata_list: + if not isinstance(metadata, (bytes, bytearray)) or not metadata.startswith( + _MAGIC_PREFIX + ): + records.append(None) + continue + + record = parse_pmu_profile_event(bytes(metadata)) + found = True + records.append( + { + "product": record.product, + "platform": record.platform, + "macs_per_cycle": record.macs_per_cycle, + "success": record.success, + "register_base": record.register_base, + "cycle_count": record.cycle_count, + "cycle_overflow": record.cycle_overflow, + "counters": [ + { + "event_type": counter.event_type, + "event_id": counter.event_type.value, + "count": counter.count, + "overflow": counter.overflow, + } + for counter in record.counters + ], + } + ) + + return {"ethosu_pmu": records} if found else {} diff --git a/backends/arm/runtime/EthosUBackend_CoreDriver.cpp b/backends/arm/runtime/EthosUBackend_CoreDriver.cpp index 5d905365329..d5e67b55b5f 100644 --- a/backends/arm/runtime/EthosUBackend_CoreDriver.cpp +++ b/backends/arm/runtime/EthosUBackend_CoreDriver.cpp @@ -54,6 +54,7 @@ namespace arm { struct PlatformState {}; +// cppcheck-suppress unusedFunction executorch::runtime::Error platform_init( executorch::runtime::ArrayRef /*specs*/, executorch::runtime::MemoryAllocator* /*allocator*/, @@ -61,14 +62,18 @@ executorch::runtime::Error platform_init( return executorch::runtime::Error::Ok; } +// cppcheck-suppress unusedFunction void platform_destroy(PlatformState* /*state*/) {} +// cppcheck-suppress unusedFunction bool needs_scratch_allocation() { return true; } +// cppcheck-suppress unusedFunction Error platform_execute( - BackendExecutionContext& /*context*/, + // cppcheck-suppress constParameterReference + [[maybe_unused]] BackendExecutionContext& context, [[maybe_unused]] const ExecutionHandle* execution_handle, const VelaHandles& handles, int input_count, @@ -137,6 +142,9 @@ Error platform_execute( executorch::backends::arm::EthosUInvocationContext invocation_context{}; invocation_context.delegate_handle = reinterpret_cast(execution_handle); +#if defined(ET_EVENT_TRACER_ENABLED) + invocation_context.event_tracer = context.event_tracer(); +#endif int result = ethosu_invoke_v3( driver.get(), static_cast(handles.cmd_data), diff --git a/backends/arm/runtime/EthosUBackend_Pmu.h b/backends/arm/runtime/EthosUBackend_Pmu.h index b80098235c6..1b026a45faa 100644 --- a/backends/arm/runtime/EthosUBackend_Pmu.h +++ b/backends/arm/runtime/EthosUBackend_Pmu.h @@ -8,6 +8,10 @@ #include +#if defined(ET_EVENT_TRACER_ENABLED) +#include +#endif + namespace executorch::backends::arm { // EthosUStatsBuffer is defined in EthosUExecutionStats.h. @@ -17,6 +21,10 @@ struct EthosUInvocationContext { EthosUStatsBuffer* buffer = nullptr; uintptr_t delegate_handle = 0; uint32_t counter_count = 0; +#if defined(ET_EVENT_TRACER_ENABLED) + executorch::runtime::EventTracer* event_tracer = nullptr; + executorch::runtime::EventTracerEntry cycle_event{}; +#endif }; bool ethosu_execution_metrics_enabled() noexcept; diff --git a/backends/arm/runtime/EthosUExecutionStats.cpp b/backends/arm/runtime/EthosUExecutionStats.cpp index 92225bea937..7c51d1762eb 100644 --- a/backends/arm/runtime/EthosUExecutionStats.cpp +++ b/backends/arm/runtime/EthosUExecutionStats.cpp @@ -13,6 +13,22 @@ #include #include +#if defined(ET_EVENT_TRACER_ENABLED) +namespace { +void write_le32(uint8_t* destination, uint32_t value) { + for (size_t byte = 0; byte < 4; ++byte) { + destination[byte] = static_cast(value >> (8 * byte)); + } +} + +void write_le64(uint8_t* destination, uint64_t value) { + for (size_t byte = 0; byte < 8; ++byte) { + destination[byte] = static_cast(value >> (8 * byte)); + } +} +} // namespace +#endif + namespace executorch::backends::arm { namespace { std::atomic capture_buffer{nullptr}; @@ -44,7 +60,11 @@ void ethosu_inference_begin(struct ethosu_driver* drv, void* user_arg) { invocation->counter_count = 0; invocation->buffer = executorch::backends::arm::capture_buffer.load(std::memory_order_acquire); - if (invocation->buffer != nullptr) { + if (invocation->buffer != nullptr +#if defined(ET_EVENT_TRACER_ENABLED) + || invocation->event_tracer != nullptr +#endif + ) { static constexpr ethosu_pmu_event_type kU55U65Events[] = { ETHOSU_PMU_AXI0_RD_DATA_BEAT_RECEIVED, ETHOSU_PMU_AXI1_RD_DATA_BEAT_RECEIVED, @@ -88,6 +108,14 @@ void ethosu_inference_begin(struct ethosu_driver* drv, void* user_arg) { ETHOSU_PMU_CYCCNT_Reset(drv); ETHOSU_PMU_EVCNTR_ALL_Reset(drv); ETHOSU_PMU_Set_CNTR_OVS(drv, event_mask | ETHOSU_PMU_CCNT_Msk); +#if defined(ET_EVENT_TRACER_ENABLED) + if (invocation->event_tracer != nullptr) { + invocation->cycle_event = + invocation->event_tracer->start_profiling_delegate( + executorch::backends::arm::kEthosUCycleEventName, + executorch::runtime::kUnsetDelegateDebugIntId); + } +#endif } } @@ -101,7 +129,7 @@ void ethosu_inference_end(struct ethosu_driver* drv, void* user_arg) { return; } - if (invocation->buffer != nullptr && invocation->counter_count != 0) { + if (invocation->counter_count != 0) { executorch::backends::arm::EthosUExecutionRecord record{}; record.delegate_handle = invocation->delegate_handle; record.success = drv->job.result == ETHOSU_JOB_RESULT_OK; @@ -120,7 +148,40 @@ void ethosu_inference_end(struct ethosu_driver* drv, void* user_arg) { } ETHOSU_PMU_Disable(drv); EthosUBackend_npu_end(); - invocation->buffer->append(record); +#if defined(ET_EVENT_TRACER_ENABLED) + if (invocation->event_tracer != nullptr) { + // Encode metadata in format described in EthosUExecutionStats.h + uint8_t metadata[executorch::backends::arm::kEthosUCycleMetadataMaxSize]{ + 'E', 'U', 'C', '1'}; + write_le32(metadata + 4, record.stats.product); + write_le32(metadata + 8, 1u << drv->dev.caps.log2_macs); + write_le32(metadata + 12, static_cast(record.stats.platform)); + const uint32_t flags = + (record.stats.cycle_overflow ? 1u : 0u) | (record.success ? 2u : 0u); + write_le32(metadata + 16, flags); + write_le64(metadata + 20, record.stats.device_identifier); + write_le64(metadata + 28, record.stats.cycle_count); + write_le32(metadata + 36, record.stats.counter_count); + for (uint32_t slot = 0; slot < record.stats.counter_count; ++slot) { + const size_t offset = + executorch::backends::arm::kEthosUCycleMetadataHeaderSize + + slot * executorch::backends::arm::kEthosUCycleCounterMetadataSize; + const auto& counter = record.stats.counters[slot]; + write_le32(metadata + offset, counter.event_id); + write_le64(metadata + offset + 4, counter.value); + write_le32(metadata + offset + 12, counter.overflow ? 1u : 0u); + } + const size_t metadata_size = + executorch::backends::arm::kEthosUCycleMetadataHeaderSize + + record.stats.counter_count * + executorch::backends::arm::kEthosUCycleCounterMetadataSize; + invocation->event_tracer->end_profiling_delegate( + invocation->cycle_event, metadata, metadata_size); + } +#endif + if (invocation->buffer != nullptr) { + invocation->buffer->append(record); + } return; } EthosUBackend_npu_end(); diff --git a/backends/arm/runtime/EthosUExecutionStats.h b/backends/arm/runtime/EthosUExecutionStats.h index fa04759f237..4e3c905255c 100644 --- a/backends/arm/runtime/EthosUExecutionStats.h +++ b/backends/arm/runtime/EthosUExecutionStats.h @@ -36,6 +36,20 @@ bool ethosu_execution_metrics_enabled() noexcept; constexpr size_t kMaxPmuEventCounters = 8; +constexpr char kEthosUCycleEventName[] = "ethosu.pmu.counters"; +// Experimental format; layout and fields may change without compatibility. +// ETDump delegate_debug_metadata starts with "EUC" and ASCII version '1' +// [0:4], followed by little-endian product [4:8], MACs per cycle [8:12], +// EthosUPlatform [12:16], flags [16:20] (bit 0: cycle overflow, +// bit 1: invocation succeeded), register base [20:28], NPU cycle count +// [28:36], and counter count [36:40]. +// Each populated counter then stores +// a symbolic event ID (4 bytes), count (8 bytes), and overflow (4 bytes). +constexpr size_t kEthosUCycleMetadataHeaderSize = 40; +constexpr size_t kEthosUCycleCounterMetadataSize = 16; +constexpr size_t kEthosUCycleMetadataMaxSize = kEthosUCycleMetadataHeaderSize + + kMaxPmuEventCounters * kEthosUCycleCounterMetadataSize; + enum class EthosUPlatform : uint8_t { CoreDriver, }; diff --git a/backends/arm/runtime/targets.bzl b/backends/arm/runtime/targets.bzl index 8ddabe90ade..75cb47c7bbc 100644 --- a/backends/arm/runtime/targets.bzl +++ b/backends/arm/runtime/targets.bzl @@ -25,6 +25,7 @@ def define_common_targets(): runtime.cxx_library( name = "ethosu_invocation_context", exported_headers = ["EthosUBackend_Pmu.h"], + exported_deps = ["//executorch/runtime/core:event_tracer"], visibility = ["PUBLIC"], ) runtime.cxx_library( diff --git a/backends/arm/runtime/tests/ethos-u/CMakeLists.txt b/backends/arm/runtime/tests/ethos-u/CMakeLists.txt index 5a2ee4240e9..dbd860a03be 100644 --- a/backends/arm/runtime/tests/ethos-u/CMakeLists.txt +++ b/backends/arm/runtime/tests/ethos-u/CMakeLists.txt @@ -92,6 +92,26 @@ target_include_directories( "${EXECUTORCH_ROOT}/.." "${EXECUTORCH_ROOT}/examples/arm/arm-scratch/ethos-u/core_software/core_driver/include" ) +target_compile_definitions( + ethosu_execution_stats_test_library PRIVATE ET_EVENT_TRACER_ENABLED +) +target_link_libraries( + ethosu_execution_stats_test_library PUBLIC executorch_core +) + +add_library( + ethosu_execution_stats_no_trace_test_library STATIC + "${EXECUTORCH_ROOT}/backends/arm/runtime/EthosUExecutionStats.cpp" +) +target_include_directories( + ethosu_execution_stats_no_trace_test_library + PRIVATE + "${EXECUTORCH_ROOT}/.." + "${EXECUTORCH_ROOT}/examples/arm/arm-scratch/ethos-u/core_software/core_driver/include" +) +target_link_libraries( + ethosu_execution_stats_no_trace_test_library PUBLIC executorch_core +) add_executable( ethosu_execution_stats_test @@ -105,8 +125,8 @@ target_include_directories( "${EXECUTORCH_ROOT}/examples/arm/arm-scratch/ethos-u/core_software/core_driver/include" ) target_link_libraries( - ethosu_execution_stats_test PRIVATE ethosu_execution_stats_test_library - Threads::Threads + ethosu_execution_stats_test + PRIVATE ethosu_execution_stats_no_trace_test_library Threads::Threads ) add_test(NAME ethosu_execution_stats COMMAND ethosu_execution_stats_test) set_tests_properties( @@ -124,6 +144,9 @@ target_include_directories( "${EXECUTORCH_ROOT}/.." "${EXECUTORCH_ROOT}/examples/arm/arm-scratch/ethos-u/core_software/core_driver/include" ) +target_compile_definitions( + ethosu_pmu_callbacks_test PRIVATE ET_EVENT_TRACER_ENABLED +) target_link_libraries( ethosu_pmu_callbacks_test PRIVATE ethosu_execution_stats_test_library Threads::Threads diff --git a/backends/arm/test/ethosu_pmu_callbacks_test.cpp b/backends/arm/test/ethosu_pmu_callbacks_test.cpp index f984be616c7..8914f60ef4b 100644 --- a/backends/arm/test/ethosu_pmu_callbacks_test.cpp +++ b/backends/arm/test/ethosu_pmu_callbacks_test.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #define CHECK(expr) \ @@ -26,6 +27,96 @@ } while (false) using namespace executorch::backends::arm; +using namespace executorch::runtime; + +class CapturingTracer final : public EventTracer { + public: + uint8_t metadata[kEthosUCycleMetadataMaxSize]{}; + size_t metadata_size = 0; + bool started = false; + bool ended = false; + + void create_event_block(const char*) override {} + EventTracerEntry start_profiling(const char*, ChainID, DebugHandle) override { + return {}; + } + EventTracerEntry start_profiling_delegate( + const char* name, + DelegateDebugIntId index) override { + CHECK(std::strcmp(name, kEthosUCycleEventName) == 0); + CHECK(index == kUnsetDelegateDebugIntId); + started = true; + EventTracerEntry entry{}; + entry.event_id = 17; + return entry; + } + void end_profiling_delegate( + EventTracerEntry entry, + const void* data, + size_t size) override { + CHECK(started); + CHECK(entry.event_id == 17); + CHECK(size <= sizeof(metadata)); + std::memcpy(metadata, data, size); + metadata_size = size; + ended = true; + } + void log_profiling_delegate( + const char*, + DelegateDebugIntId, + et_timestamp_t, + et_timestamp_t, + const void*, + size_t) override {} + void end_profiling(EventTracerEntry) override {} + void track_allocation(AllocatorID, size_t) override {} + AllocatorID track_allocator(const char*) override { + return 0; + } + Result log_evalue(const EValue&, LoggedEValueType) override { + return true; + } + Result log_intermediate_output_delegate( + const char*, + DelegateDebugIntId, + const executorch::aten::Tensor&) override { + return true; + } + Result log_intermediate_output_delegate( + const char*, + DelegateDebugIntId, + const ArrayRef) override { + return true; + } + Result log_intermediate_output_delegate( + const char*, + DelegateDebugIntId, + const int&) override { + return true; + } + Result log_intermediate_output_delegate( + const char*, + DelegateDebugIntId, + const bool&) override { + return true; + } + Result log_intermediate_output_delegate( + const char*, + DelegateDebugIntId, + const double&) override { + return true; + } + void set_delegation_intermediate_output_filter( + EventTracerFilterBase*) override {} +}; + +uint64_t read_le(const uint8_t* bytes, size_t size) { + uint64_t value = 0; + for (size_t index = 0; index < size; ++index) { + value |= static_cast(bytes[index]) << (8 * index); + } + return value; +} extern "C" { void ethosu_inference_begin(ethosu_driver*, void*); @@ -112,5 +203,69 @@ int main() { CHECK(observer_ends == 3); CHECK(set_ethosu_stats_buffer(nullptr) == &buffer); + + CapturingTracer tracer; + EthosUInvocationContext traced_invocation{}; + traced_invocation.event_tracer = &tracer; + ethosu_inference_begin(&u85, &traced_invocation); + CHECK(pmu(&u85).enabled); + pmu(&u85).cycles = 123456; + pmu(&u85).counts[0] = 11; + pmu(&u85).counts[5] = 67; + pmu(&u85).overflow = ETHOSU_PMU_CCNT_Msk | (1u << 5); + ethosu_inference_end(&u85, &traced_invocation); + CHECK(!pmu(&u85).enabled); + CHECK(tracer.started && tracer.ended); + CHECK( + tracer.metadata_size == + kEthosUCycleMetadataHeaderSize + 7 * kEthosUCycleCounterMetadataSize); + CHECK(std::memcmp(tracer.metadata, "EUC1", 4) == 0); + CHECK(read_le(tracer.metadata + 4, 4) == 2); + CHECK(read_le(tracer.metadata + 8, 4) == 256); + CHECK( + read_le(tracer.metadata + 12, 4) == + static_cast(EthosUPlatform::CoreDriver)); + CHECK(read_le(tracer.metadata + 16, 4) == 1); + CHECK(read_le(tracer.metadata + 20, 8) == 0x50000000); + CHECK(read_le(tracer.metadata + 28, 8) == 123456); + CHECK(read_le(tracer.metadata + 36, 4) == 7); + for (size_t slot = 0; slot < 7; ++slot) { + const size_t offset = + kEthosUCycleMetadataHeaderSize + slot * kEthosUCycleCounterMetadataSize; + CHECK(read_le(tracer.metadata + offset, 4) == pmu(&u85).events[slot]); + CHECK(read_le(tracer.metadata + offset + 4, 8) == pmu(&u85).counts[slot]); + CHECK(read_le(tracer.metadata + offset + 12, 4) == (slot == 5)); + } + + CapturingTracer other_tracer; + EthosUInvocationContext other_invocation{}; + other_invocation.event_tracer = &other_tracer; + ethosu_inference_begin(&u55, &other_invocation); + pmu(&u55).cycles = 45; + pmu(&u55).counts[3] = 9; + pmu(&u55).overflow = 1u << 3; + ethosu_inference_end(&u55, &other_invocation); + CHECK(other_tracer.started && other_tracer.ended); + CHECK( + other_tracer.metadata_size == + kEthosUCycleMetadataHeaderSize + 4 * kEthosUCycleCounterMetadataSize); + CHECK(read_le(other_tracer.metadata + 8, 4) == 128); + CHECK( + read_le(other_tracer.metadata + 12, 4) == + static_cast(EthosUPlatform::CoreDriver)); + CHECK(read_le(other_tracer.metadata + 16, 4) == 2); + CHECK(read_le(other_tracer.metadata + 20, 8) == 0x40000000); + CHECK(read_le(other_tracer.metadata + 28, 8) == 45); + CHECK(read_le(other_tracer.metadata + 36, 4) == 4); + for (size_t slot = 0; slot < 4; ++slot) { + const size_t offset = + kEthosUCycleMetadataHeaderSize + slot * kEthosUCycleCounterMetadataSize; + CHECK(read_le(other_tracer.metadata + offset, 4) == pmu(&u55).events[slot]); + CHECK( + read_le(other_tracer.metadata + offset + 4, 8) == + pmu(&u55).counts[slot]); + CHECK(read_le(other_tracer.metadata + offset + 12, 4) == (slot == 3)); + } + CHECK(buffer.size == 2); std::puts("Ethos-U PMU callbacks: PASS"); } diff --git a/backends/arm/test/misc/test_parse_ethosu_delegate_metadata.py b/backends/arm/test/misc/test_parse_ethosu_delegate_metadata.py new file mode 100644 index 00000000000..bf03f80d5db --- /dev/null +++ b/backends/arm/test/misc/test_parse_ethosu_delegate_metadata.py @@ -0,0 +1,150 @@ +# 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. + +import math +import struct +import unittest + +from executorch.backends.arm.ethosu.parse_delegate_metadata import ( + _COUNTER, + _HEADER, + _MAGIC_PREFIX, + EthosUPlatform, + EthosUPmuEvent, + EthosUProduct, + parse_ethosu_delegate_pmu_metadata, + parse_pmu_profile_event, +) + + +def _metadata( + *counters: tuple[int, int, int], + flags: int = 2, + product: int = 2, + platform: int = 0, + macs_per_cycle: int = 256, +) -> bytes: + header = _HEADER.pack( + _MAGIC_PREFIX + b"1", + product, + macs_per_cycle, + platform, + flags, + 0x50000000, + 123456, + len(counters), + ) + return header + b"".join(_COUNTER.pack(*counter) for counter in counters) + + +class TestEthosUDelegateMetadata(unittest.TestCase): + """Check Ethos-U PMU metadata decoding and Inspector adaptation.""" + + def test_decodes_product(self) -> None: + """Map each core-driver product ID to its enum value.""" + for product in EthosUProduct: + with self.subTest(product=product): + self.assertIs( + parse_pmu_profile_event(_metadata(product=product.value)).product, + product, + ) + + def test_decodes_profile_event(self) -> None: + """Decode counters into their symbolic event types.""" + metadata = _metadata((97, 67, 1), (178, 11, 0)) + + record = parse_pmu_profile_event(metadata) + + self.assertIs(record.product, EthosUProduct.U85) + self.assertIs(record.platform, EthosUPlatform.CoreDriver) + self.assertEqual(record.macs_per_cycle, 256) + self.assertTrue(record.success) + self.assertEqual(record.register_base, 0x50000000) + self.assertEqual(record.cycle_count, 123456) + self.assertIsInstance(record.cycle_count, float) + self.assertFalse(record.cycle_overflow) + self.assertEqual(len(record.counters), 2) + self.assertEqual( + record.counters[0].event_type, EthosUPmuEvent.ETHOSU_PMU_MAC_ACTIVE + ) + self.assertTrue(math.isnan(record.counters[0].count)) + self.assertTrue(record.counters[0].overflow) + self.assertEqual( + record.counters[1].event_type, EthosUPmuEvent.ETHOSU_PMU_WD_ACTIVE + ) + self.assertFalse(record.counters[1].overflow) + self.assertEqual(record.counters[1].count, 11) + self.assertIsInstance(record.counters[1].count, float) + + def test_cycle_overflow_returns_nan(self) -> None: + """Use NaN when the cycle counter overflows.""" + record = parse_pmu_profile_event(_metadata(flags=3)) + + self.assertTrue(record.cycle_overflow) + self.assertTrue(math.isnan(record.cycle_count)) + + def test_inspector_adapter_preserves_sample_positions(self) -> None: + """Keep sample indices when unrelated or empty entries occur.""" + metadata = _metadata((112, 45, 0)) + + parsed = parse_ethosu_delegate_pmu_metadata([b"", b"other", metadata]) + + self.assertEqual(parsed["ethosu_pmu"][:2], [None, None]) + sample = parsed["ethosu_pmu"][2] + self.assertIs(sample["product"], EthosUProduct.U85) + self.assertIs(sample["platform"], EthosUPlatform.CoreDriver) + self.assertEqual(sample["macs_per_cycle"], 256) + self.assertEqual(sample["cycle_count"], 123456) + self.assertEqual(len(sample["counters"]), 1) + counter = sample["counters"][0] + self.assertIs(counter["event_type"], EthosUPmuEvent.ETHOSU_PMU_NPU_IDLE) + self.assertEqual(counter["event_id"], 112) + self.assertEqual(counter["count"], 45) + self.assertFalse(counter["overflow"]) + self.assertEqual(parse_ethosu_delegate_pmu_metadata([b"other", ""]), {}) + + def test_rejects_invalid_metadata(self) -> None: + """Reject malformed Ethos-U records and unsupported event IDs.""" + valid = _metadata((97, 67, 1)) + invalid = { + "short header": (valid[:39], "shorter than its header"), + "wrong magic": (b"XYZ1" + valid[4:], "magic"), + "unsupported version": ( + b"EUC2" + valid[4:], + r"supported versions: \{1\}", + ), + "invalid version": (b"EUCx" + valid[4:], "version"), + "extra byte": (valid + b"\x00", "expected"), + "too many counters": ( + valid[:36] + struct.pack("