From c2937d2114d13584062f6179cec2400f1ea2d60e Mon Sep 17 00:00:00 2001 From: Elena Zhelezina Date: Tue, 30 Jun 2026 21:32:51 +0100 Subject: [PATCH] Arm backend: Dump Arm VGF Delegate Boundary Inputs Signed-off-by: Elena Zhelezina Change-Id: Ie5c647024179cb08c92f0a60224458da2d6c626c --- CMakeLists.txt | 9 + backends/arm/runtime/VGFBackend.cpp | 327 ++++++++++++++++++ .../executor_runner/executor_runner.cpp | 82 +++++ 3 files changed, 418 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d1171c46d6..2f4caa1ec62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -229,6 +229,12 @@ if(EXECUTORCH_BUILD_CUDA) add_definitions(-DCUDA_AVAILABLE=1) endif() +option(ET_ENABLE_VGF_INPUT_DUMP + "Enable Arm/VGF delegate-boundary input dump flags in executor_runner" + OFF +) +announce_configured_options(ET_ENABLE_VGF_INPUT_DUMP) + # -ffunction-sections -fdata-sections: breaks function and data into sections so # they can be properly gc'd. -s: strip symbol. if(WIN32) @@ -1423,6 +1429,9 @@ if(EXECUTORCH_BUILD_EXECUTOR_RUNNER) endif() target_link_libraries(executor_runner PRIVATE ${_executor_runner_libs}) target_compile_options(executor_runner PUBLIC ${_common_compile_options}) + if(ET_ENABLE_VGF_INPUT_DUMP) + target_compile_definitions(executor_runner PRIVATE ET_ENABLE_VGF_INPUT_DUMP) + endif() if(EXECUTORCH_BUILD_ARM_ETHOSU_LINUX) target_compile_definitions( executor_runner PRIVATE EXECUTORCH_BUILD_ARM_ETHOSU_LINUX=1 diff --git a/backends/arm/runtime/VGFBackend.cpp b/backends/arm/runtime/VGFBackend.cpp index 0f6893d1dec..3553914a5c7 100644 --- a/backends/arm/runtime/VGFBackend.cpp +++ b/backends/arm/runtime/VGFBackend.cpp @@ -5,9 +5,20 @@ * LICENSE file in the root directory of this source tree. */ +#include +#include +#include #include +#include +#include +#include +#include +#include +#include #include #include +#include +#include using namespace std; @@ -15,6 +26,7 @@ using namespace std; #include #include #include +#include #ifdef ET_EVENT_TRACER_ENABLED #include @@ -33,6 +45,7 @@ using executorch::runtime::FreeableBuffer; using executorch::runtime::MemoryAllocator; using executorch::runtime::Result; using executorch::runtime::Span; +using executorch::runtime::toString; #ifdef ET_EVENT_TRACER_ENABLED using executorch::runtime::event_tracer_end_profiling_delegate; @@ -101,6 +114,301 @@ void vkml_free_basics( // vkDestroyInstance(*instance, nullptr); } +// Helper functions to dump VGF Delegate Boundary Inputs +constexpr const char* kVgfDumpInputsDirEnv = "EXECUTORCH_VGF_DUMP_INPUTS_DIR"; +constexpr const char* kVgfDumpInputsAndExitEnv = + "EXECUTORCH_VGF_DUMP_INPUTS_AND_EXIT"; + +std::atomic g_vgf_dump_invocation{0}; + +bool env_flag_enabled(const char* name) { + const char* value = std::getenv(name); + return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0 && + std::strcmp(value, "false") != 0 && std::strcmp(value, "False") != 0; +} + +const char* descriptor_type_to_string(VkDescriptorType type) { + switch (type) { + case VK_DESCRIPTOR_TYPE_TENSOR_ARM: + return "VK_DESCRIPTOR_TYPE_TENSOR_ARM"; + case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: + return "VK_DESCRIPTOR_TYPE_STORAGE_BUFFER"; + case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER: + return "VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER"; + case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: + return "VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE"; + case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: + return "VK_DESCRIPTOR_TYPE_STORAGE_IMAGE"; + default: + return "VK_DESCRIPTOR_TYPE_UNKNOWN"; + } +} + +template +std::string array_ref_to_json(ArrayRef values) { + std::ostringstream out; + out << "["; + for (size_t i = 0; i < values.size(); ++i) { + if (i != 0) { + out << ", "; + } + out << static_cast(values[i]); + } + out << "]"; + return out.str(); +} + +template +std::string vector_to_json(const std::vector& values) { + std::ostringstream out; + out << "["; + for (size_t i = 0; i < values.size(); ++i) { + if (i != 0) { + out << ", "; + } + out << static_cast(values[i]); + } + out << "]"; + return out.str(); +} + +bool write_binary_file( + const std::filesystem::path& path, + const void* data, + size_t nbytes) { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + if (!file) { + return false; + } + + file.write( + static_cast(data), static_cast(nbytes)); + return file.good(); +} + +std::string make_vgf_call_dir_name(uint64_t invocation) { + std::ostringstream call_name; + call_name << "call_" << std::setw(6) << std::setfill('0') << invocation; + return call_name.str(); +} + +std::filesystem::path make_vgf_tmp_call_dir( + const std::filesystem::path& dump_root, + const std::string& call_dir_name) { + const auto nonce = + std::chrono::steady_clock::now().time_since_epoch().count(); + + std::ostringstream tmp_name; + tmp_name << "." << call_dir_name << ".tmp." << nonce; + + return dump_root / tmp_name.str(); +} + +class ScopedTmpDir { + public: + explicit ScopedTmpDir(std::filesystem::path path) + : path_(std::move(path)), active_(true) {} + + ~ScopedTmpDir() { + if (active_) { + std::error_code ec; + std::filesystem::remove_all(path_, ec); + } + } + + ScopedTmpDir(const ScopedTmpDir&) = delete; + ScopedTmpDir& operator=(const ScopedTmpDir&) = delete; + + void release() { + active_ = false; + } + + private: + std::filesystem::path path_; + bool active_; +}; + +Error dump_vgf_delegate_inputs( + const VgfRepr& repr, + Span args, + const char* dump_root) { + const uint64_t invocation = g_vgf_dump_invocation.fetch_add(1); + + const std::filesystem::path dump_root_path(dump_root); + const std::string call_dir_name = make_vgf_call_dir_name(invocation); + const std::filesystem::path final_call_dir = dump_root_path / call_dir_name; + const std::filesystem::path tmp_call_dir = + make_vgf_tmp_call_dir(dump_root_path, call_dir_name); + + std::error_code ec; + + std::filesystem::create_directories(dump_root_path, ec); + if (ec) { + ET_LOG( + Error, + "Failed to create VGF dump root %s: %s", + dump_root_path.string().c_str(), + ec.message().c_str()); + return Error::Internal; + } + + if (std::filesystem::exists(final_call_dir, ec)) { + ET_LOG( + Error, + "VGF dump output directory already exists: %s", + final_call_dir.string().c_str()); + return Error::Internal; + } + + std::filesystem::create_directory(tmp_call_dir, ec); + if (ec) { + ET_LOG( + Error, + "Failed to create temporary VGF dump directory %s: %s", + tmp_call_dir.string().c_str(), + ec.message().c_str()); + return Error::Internal; + } + + ScopedTmpDir tmp_dir_guard(tmp_call_dir); + + std::vector input_file_names; + input_file_names.reserve(repr.model_input_count); + + for (size_t input_arg_idx = 0; input_arg_idx < repr.model_input_count; + ++input_arg_idx) { + const int io_idx = repr.model_input_io_index[input_arg_idx]; + + if (io_idx < 0 || static_cast(io_idx) >= repr.IOs.size()) { + ET_LOG( + Error, + "Invalid VGF input IO index %d for input arg %zu", + io_idx, + input_arg_idx); + return Error::InvalidArgument; + } + + if (args[input_arg_idx] == nullptr || !args[input_arg_idx]->isTensor()) { + ET_LOG(Error, "VGF input arg %zu is not a tensor", input_arg_idx); + return Error::InvalidArgument; + } + + const Tensor& tensor = args[input_arg_idx]->toTensor(); + const IO& io = repr.IOs[io_idx]; + + if (tensor.nbytes() != io.allocation_size) { + ET_LOG( + Error, + "VGF input arg %zu size mismatch: tensor nbytes=%zu, IO allocation_size=%zu", + input_arg_idx, + tensor.nbytes(), + io.allocation_size); + return Error::InvalidArgument; + } + + std::ostringstream file_name; + file_name << "input_" << std::setw(3) << std::setfill('0') << input_arg_idx + << "_io_" << io_idx << ".bin"; + + input_file_names.push_back(file_name.str()); + + const std::filesystem::path input_path = tmp_call_dir / file_name.str(); + + if (!write_binary_file( + input_path, tensor.const_data_ptr(), tensor.nbytes())) { + ET_LOG( + Error, + "Failed to write VGF input dump file %s", + input_path.string().c_str()); + return Error::Internal; + } + } + + const std::filesystem::path metadata_path = tmp_call_dir / "metadata.json"; + { + std::ofstream metadata(metadata_path, std::ios::out | std::ios::trunc); + if (!metadata) { + ET_LOG( + Error, + "Failed to open VGF metadata file %s", + metadata_path.string().c_str()); + return Error::Internal; + } + + metadata << "{\n"; + metadata << " \"format_version\": 1,\n"; + metadata << " \"invocation\": " << invocation << ",\n"; + metadata << " \"input_count\": " << repr.model_input_count << ",\n"; + metadata << " \"inputs\": [\n"; + + for (size_t input_arg_idx = 0; input_arg_idx < repr.model_input_count; + ++input_arg_idx) { + const int io_idx = repr.model_input_io_index[input_arg_idx]; + const Tensor& tensor = args[input_arg_idx]->toTensor(); + const IO& io = repr.IOs[io_idx]; + + metadata << " {\n"; + metadata << " \"arg_index\": " << input_arg_idx << ",\n"; + metadata << " \"io_index\": " << io_idx << ",\n"; + metadata << " \"file\": \"" << input_file_names[input_arg_idx] + << "\",\n"; + metadata << " \"nbytes\": " << tensor.nbytes() << ",\n"; + metadata << " \"scalar_type\": \"" << toString(tensor.scalar_type()) + << "\",\n"; + metadata << " \"tensor_shape\": " + << array_ref_to_json(tensor.sizes()) << ",\n"; + metadata << " \"tensor_strides\": " + << array_ref_to_json(tensor.strides()) << ",\n"; + metadata << " \"io_shape\": " << vector_to_json(io.size) << ",\n"; + metadata << " \"io_strides\": " << vector_to_json(io.stride) + << ",\n"; + metadata << " \"io_element_size\": " << io.elt_size << ",\n"; + metadata << " \"io_allocation_size\": " << io.allocation_size + << ",\n"; + metadata << " \"io_descriptor_type\": \"" + << descriptor_type_to_string(io.descriptor_type) << "\"\n"; + metadata << " }"; + + if (input_arg_idx + 1 < repr.model_input_count) { + metadata << ","; + } + metadata << "\n"; + } + + metadata << " ]\n"; + metadata << "}\n"; + + metadata.close(); + if (!metadata) { + ET_LOG( + Error, + "Failed to write VGF metadata file %s", + metadata_path.string().c_str()); + return Error::Internal; + } + } + + std::filesystem::rename(tmp_call_dir, final_call_dir, ec); + if (ec) { + ET_LOG( + Error, + "Failed to publish VGF dump directory %s -> %s: %s", + tmp_call_dir.string().c_str(), + final_call_dir.string().c_str(), + ec.message().c_str()); + return Error::Internal; + } + + tmp_dir_guard.release(); + + ET_LOG( + Info, + "Wrote VGF delegate input dump to %s", + final_call_dir.string().c_str()); + + return Error::Ok; +} + class VGFBackend final : public ::executorch::runtime::BackendInterface { public: VGFBackend() = default; @@ -264,6 +572,25 @@ class VGFBackend final : public ::executorch::runtime::BackendInterface { return Error::InvalidArgument; } + // Helper block to dump VGF delegate boundary inputs for testing + // with scenari0 runner + const char* dump_inputs_dir = std::getenv(kVgfDumpInputsDirEnv); + if (dump_inputs_dir != nullptr && dump_inputs_dir[0] != '\0') { + Error dump_status = + dump_vgf_delegate_inputs(*repr, args, dump_inputs_dir); + if (dump_status != Error::Ok) { + return dump_status; + } + + if (env_flag_enabled(kVgfDumpInputsAndExitEnv)) { + ET_LOG( + Info, + "Exiting after VGF delegate input dump because %s is set", + kVgfDumpInputsAndExitEnv); + return Error::EndOfMethod; + } + } + #ifdef ET_EVENT_TRACER_ENABLED EventTracer* event_tracer = context.event_tracer(); diff --git a/examples/portable/executor_runner/executor_runner.cpp b/examples/portable/executor_runner/executor_runner.cpp index 4f2de438bfe..72c4d40d01d 100644 --- a/examples/portable/executor_runner/executor_runner.cpp +++ b/examples/portable/executor_runner/executor_runner.cpp @@ -22,6 +22,7 @@ #ifdef ET_BUNDLE_IO_ENABLED #include #endif // ET_BUNDLE_IO_ENABLED +#include #include #include #include @@ -92,6 +93,18 @@ DEFINE_int32( cpu_threads, -1, "Number of CPU threads for inference. Defaults to -1, which implies we'll use a heuristic to derive the # of performant cores for a specific device."); +#if defined(ET_ENABLE_VGF_INPUT_DUMP) +DEFINE_string( + vgf_input_dump_dir, + "", + "Directory for Arm VGF delegate-boundary input dumps. Empty disables dumping."); + +DEFINE_bool( + dump_vgf_inputs_and_exit, + false, + "Dump Arm VGF delegate-boundary inputs from the first VGFBackend::execute() " + "call and stop before VGF dispatch. Requires --vgf_input_dump_dir."); +#endif #ifdef ET_BUNDLE_IO_ENABLED DEFINE_double(bundleio_rtol, 0.01, "Relative tolerance for bundled IO."); @@ -168,6 +181,47 @@ Error load_input_files( return Error::Ok; } +#if defined(ET_ENABLE_VGF_INPUT_DUMP) +constexpr const char* kVgfDumpInputsDirEnv = "EXECUTORCH_VGF_DUMP_INPUTS_DIR"; +constexpr const char* kVgfDumpInputsAndExitEnv = + "EXECUTORCH_VGF_DUMP_INPUTS_AND_EXIT"; +Error set_env_var(const char* name, const std::string& value) { +#if defined(_WIN32) + return _putenv_s(name, value.c_str()) == 0 ? Error::Ok : Error::Internal; +#else + return setenv(name, value.c_str(), 1) == 0 ? Error::Ok : Error::Internal; +#endif +} +#endif + +bool configure_vgf_input_dump_flags() { +#if defined(ET_ENABLE_VGF_INPUT_DUMP) + if (FLAGS_dump_vgf_inputs_and_exit && FLAGS_vgf_input_dump_dir.empty()) { + ET_LOG( + Error, + "--dump_vgf_inputs_and_exit requires --vgf_input_dump_dir to be set."); + return false; + } + + if (!FLAGS_vgf_input_dump_dir.empty()) { + Error status = set_env_var(kVgfDumpInputsDirEnv, FLAGS_vgf_input_dump_dir); + if (status != Error::Ok) { + ET_LOG(Error, "Failed to set %s.", kVgfDumpInputsDirEnv); + return false; + } + + status = set_env_var( + kVgfDumpInputsAndExitEnv, FLAGS_dump_vgf_inputs_and_exit ? "1" : "0"); + if (status != Error::Ok) { + ET_LOG(Error, "Failed to set %s.", kVgfDumpInputsAndExitEnv); + return false; + } + } +#endif + + return true; +} + } // namespace /// Helper to manage resources for ETDump generation @@ -239,6 +293,15 @@ std::vector try_load_file(const std::filesystem::path& path) { } #endif +bool is_expected_vgf_dump_stop(Error status) { +#if defined(ET_ENABLE_VGF_INPUT_DUMP) + return FLAGS_dump_vgf_inputs_and_exit && status == Error::EndOfMethod; +#else + (void)status; + return false; +#endif +} + int main(int argc, char** argv) { executorch::runtime::runtime_init(); @@ -267,6 +330,10 @@ int main(int argc, char** argv) { return 1; } + if (!configure_vgf_input_dump_flags()) { + return 1; + } + #if defined(ET_USE_THREADPOOL) auto cpu_threads = FLAGS_cpu_threads; uint32_t num_performant_cores = cpu_threads == -1 @@ -611,6 +678,14 @@ int main(int argc, char** argv) { } Error status = method->execute(); + + if (is_expected_vgf_dump_stop(status)) { + ET_LOG( + Info, + "VGF delegate inputs dumped successfully; exiting before delegate dispatch."); + return 0; + } + ET_CHECK_MSG( status == Error::Ok, "Execution of method %s failed with status 0x%" PRIx32, @@ -686,6 +761,13 @@ int main(int argc, char** argv) { const et_timestamp_t before_execute = executorch::runtime::pal_current_ticks(); Error status = method->execute(); + + if (is_expected_vgf_dump_stop(status)) { + ET_LOG( + Info, + "Arm/VGF delegate inputs dumped successfully; stopping before delegate dispatch."); + return 0; + } const et_timestamp_t after_execute = executorch::runtime::pal_current_ticks(); const et_timestamp_t iter_elapsed = after_execute - before_execute;