diff --git a/backends/arm/CMakeLists.txt b/backends/arm/CMakeLists.txt index 3bd07e78f07..2cc8c31790f 100644 --- a/backends/arm/CMakeLists.txt +++ b/backends/arm/CMakeLists.txt @@ -90,6 +90,13 @@ if(EXECUTORCH_BUILD_ARM_BAREMETAL OR EXECUTORCH_BUILD_ARM_ETHOSU_LINUX) executorch_delegate_ethos_u PRIVATE ${DRIVER_ETHOSU_INCLUDE_DIR} ) target_link_libraries(executorch_delegate_ethos_u PUBLIC ethosu_core_driver) + add_library( + ethosu_execution_stats STATIC + ${EXECUTORCH_ROOT}/backends/arm/runtime/EthosUExecutionStats.cpp + ) + target_include_directories( + ethosu_execution_stats PRIVATE ${_common_include_directories} + ) elseif(EXECUTORCH_BUILD_ARM_ETHOSU_LINUX) target_sources( executorch_delegate_ethos_u diff --git a/backends/arm/runtime/EthosUExecutionStats.cpp b/backends/arm/runtime/EthosUExecutionStats.cpp new file mode 100644 index 00000000000..dfa6e801301 --- /dev/null +++ b/backends/arm/runtime/EthosUExecutionStats.cpp @@ -0,0 +1,7 @@ +/* + * 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. + */ +#include diff --git a/backends/arm/runtime/EthosUExecutionStats.h b/backends/arm/runtime/EthosUExecutionStats.h new file mode 100644 index 00000000000..98e6d255dae --- /dev/null +++ b/backends/arm/runtime/EthosUExecutionStats.h @@ -0,0 +1,114 @@ +/* + * 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. + */ +#pragma once + +#include +#include +#include +#include + +namespace executorch::backends::arm { + +// Containers of Ethos-U metrics. +// An EthosUStatsBuffer contains many EthosUExecutionRecords. +// An EthosUExecutionRecord contains delegate id and status, and an +// EthosUExecutionStats, containing Ethos-U specific metrics. +struct EthosUExecutionStats; +struct EthosUExecutionRecord; +struct EthosUStatsBuffer; + +constexpr size_t kMaxPmuEventCounters = 8; + +enum class EthosUPlatform : uint8_t { + CoreDriver, +}; + +struct EthosUPmuCounter { + uint32_t event_id = 0; + uint64_t value = 0; + bool overflow = false; +}; + +struct EthosUExecutionStats { + // Register base address of the physical NPU that produced this record. + uintptr_t device_identifier = 0; + // Ethos-U product identifier for the NPU that produced this record. + uint32_t product = 0; + // Platform that produced this record. + EthosUPlatform platform = EthosUPlatform::CoreDriver; + // NPU PMU cycle count for this execution, when available. + uint64_t cycle_count = 0; + // Whether cycle_count was measured; zero can be a valid measurement. + bool cycle_count_valid = false; + // Whether the cycle counter overflowed, if overflow status is available. + bool cycle_overflow = false; + // Number of populated entries at the start of counters. + uint32_t counter_count = 0; + // PMU event IDs, counts, and overflow indications for this execution. + std::array counters{}; + + void reset() { + *this = {}; + } +}; + +struct EthosUExecutionRecord { + EthosUExecutionStats stats; + uintptr_t delegate_handle = 0; + bool success = false; +}; + +// Appends from concurrent invocations are synchronized. Read the records only +// after those invocations have completed. +struct EthosUStatsBuffer { + // Records are added at the start of the supplied array. + EthosUExecutionRecord* records = nullptr; + size_t capacity = 0; + size_t size = 0; + bool overflow = false; + + constexpr EthosUStatsBuffer() = default; + constexpr EthosUStatsBuffer( + EthosUExecutionRecord* records, + size_t capacity, + bool overflow = false) noexcept + : records(records), capacity(capacity), overflow(overflow) {} + + void append(const EthosUExecutionRecord& record) noexcept { + lock(); + if (size >= capacity) { + overflow = true; + } else { + records[size++] = record; + } + unlock(); + } + + // Read records or reset this buffer only after all publishers have completed. + bool reset() noexcept { + lock(); + const bool had_overflow = overflow; + size = 0; + overflow = false; + unlock(); + return had_overflow; + } + + private: + void lock() noexcept { + while (append_lock_.test_and_set(std::memory_order_acquire)) { + } + } + + void unlock() noexcept { + append_lock_.clear(std::memory_order_release); + } + + std::atomic_flag append_lock_ = ATOMIC_FLAG_INIT; +}; + +} // namespace executorch::backends::arm diff --git a/backends/arm/runtime/tests/ethos-u/CMakeLists.txt b/backends/arm/runtime/tests/ethos-u/CMakeLists.txt index b63178b618b..cf3ca5969c1 100644 --- a/backends/arm/runtime/tests/ethos-u/CMakeLists.txt +++ b/backends/arm/runtime/tests/ethos-u/CMakeLists.txt @@ -81,6 +81,21 @@ set_tests_properties( vela_external_blocks PROPERTIES LABELS "arm;ethos-u;runtime" ) +find_package(Threads REQUIRED) +add_executable( + ethosu_execution_stats_test + "${EXECUTORCH_ROOT}/backends/arm/runtime/EthosUExecutionStats.cpp" + "${EXECUTORCH_ROOT}/backends/arm/test/ethosu_execution_stats_test.cpp" +) +target_include_directories( + ethosu_execution_stats_test PRIVATE "${EXECUTORCH_ROOT}/.." +) +target_link_libraries(ethosu_execution_stats_test PRIVATE Threads::Threads) +add_test(NAME ethosu_execution_stats COMMAND ethosu_execution_stats_test) +set_tests_properties( + ethosu_execution_stats PROPERTIES LABELS "arm;ethos-u;runtime" +) + add_test( NAME memory_allocation_baseline COMMAND "${_ethos_u_runtime_tests_dir}/test_memory_allocation_baseline.sh" diff --git a/backends/arm/test/ethosu_execution_stats_test.cpp b/backends/arm/test/ethosu_execution_stats_test.cpp new file mode 100644 index 00000000000..faf7ecb681f --- /dev/null +++ b/backends/arm/test/ethosu_execution_stats_test.cpp @@ -0,0 +1,103 @@ +/* + * 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. + */ +#include + +#include +#include +#include + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::fprintf(stderr, "check failed at line %d: %s\n", __LINE__, #expr); \ + std::abort(); \ + } \ + } while (false) + +using namespace executorch::backends::arm; + +int main() { + EthosUExecutionRecord records[2]; + EthosUStatsBuffer buffer{records, 2}; + EthosUExecutionRecord u55{}; + u55.delegate_handle = 10; + u55.success = true; + u55.stats.device_identifier = 0x40000000; + u55.stats.product = 0; + u55.stats.cycle_count = 100; + u55.stats.cycle_count_valid = true; + u55.stats.counter_count = 4; + u55.stats.counters[0] = {17, 42, false}; + + buffer.append(u55); + CHECK(buffer.size == 1); + + EthosUExecutionRecord u85{}; + u85.delegate_handle = 20; + u85.stats.device_identifier = 0x50000000; + u85.stats.product = 2; + u85.stats.counter_count = 8; + u85.stats.counters[7] = {22, 99, false}; + + buffer.append(u85); + buffer.append(u55); + CHECK(buffer.size == 2 && buffer.overflow); + CHECK(records[0].stats.device_identifier == 0x40000000); + CHECK(records[0].stats.counter_count == 4); + CHECK(records[0].stats.counters[0].event_id == 17); + CHECK(records[1].stats.device_identifier == 0x50000000); + CHECK(records[1].stats.platform == EthosUPlatform::CoreDriver); + CHECK(records[1].stats.counter_count == 8); + CHECK(records[1].stats.counters[7].value == 99); + CHECK(!records[1].success); + CHECK(buffer.reset()); + CHECK(buffer.size == 0 && !buffer.overflow); + buffer.append(u55); + CHECK(buffer.size == 1); + CHECK(!buffer.reset()); + + constexpr size_t kThreads = 4; + constexpr size_t kRecordsPerThread = 8; + EthosUExecutionRecord concurrent_records[kThreads * kRecordsPerThread]; + EthosUStatsBuffer concurrent_buffer{ + concurrent_records, kThreads * kRecordsPerThread}; + std::thread publishers[kThreads]; + for (size_t thread = 0; thread < kThreads; ++thread) { + publishers[thread] = std::thread([&, thread] { + for (size_t index = 0; index < kRecordsPerThread; ++index) { + auto record = u55; + record.delegate_handle = thread * kRecordsPerThread + index; + concurrent_buffer.append(record); + } + }); + } + for (auto& publisher : publishers) { + publisher.join(); + } + CHECK(concurrent_buffer.size == kThreads * kRecordsPerThread); + CHECK(!concurrent_buffer.overflow); + bool seen[kThreads * kRecordsPerThread] = {}; + for (size_t index = 0; index < concurrent_buffer.size; ++index) { + const auto handle = concurrent_records[index].delegate_handle; + CHECK(handle < kThreads * kRecordsPerThread); + CHECK(!seen[handle]); + seen[handle] = true; + } + for (bool found : seen) { + CHECK(found); + } + std::thread overflow_publishers[kThreads]; + for (auto& publisher : overflow_publishers) { + publisher = std::thread([&] { concurrent_buffer.append(u85); }); + } + for (auto& publisher : overflow_publishers) { + publisher.join(); + } + CHECK(concurrent_buffer.size == kThreads * kRecordsPerThread); + CHECK(concurrent_buffer.reset()); + std::puts("Ethos-U execution stats: PASS"); +}