From 5c075eac1c3458bf0d27ccbb5ea32cdd677830b3 Mon Sep 17 00:00:00 2001 From: Elena Zhelezina Date: Wed, 15 Jul 2026 16:24:22 +0100 Subject: [PATCH 1/6] Arm backend: Add shared GPU runtime Signed-off-by: Elena Zhelezina Change-Id: Ibef2083a96af6d315f7c94027bdbed206c983275 --- CMakeLists.txt | 3 + backends/gpu_shared/BUCK | 11 + backends/gpu_shared/CMakeLists.txt | 93 ++++ backends/gpu_shared/README.md | 61 +++ .../gpu_shared/runtime/SharedGpuContext.cpp | 63 +++ .../gpu_shared/runtime/SharedGpuContext.h | 100 +++++ .../runtime/SharedGpuContextRegistry.cpp | 209 +++++++++ .../runtime/SharedGpuContextRegistry.h | 74 ++++ .../runtime/SharedGpuRuntimeConfig.cpp | 79 ++++ .../runtime/SharedGpuRuntimeConfig.h | 62 +++ backends/gpu_shared/runtime/export.h | 21 + .../gpu_shared/runtime/test/CMakeLists.txt | 23 + .../test/SharedGpuContextRegistryTest.cpp | 414 ++++++++++++++++++ .../test/SharedGpuRuntimeConfigTest.cpp | 117 +++++ backends/gpu_shared/targets.bzl | 51 +++ 15 files changed, 1381 insertions(+) create mode 100644 backends/gpu_shared/BUCK create mode 100644 backends/gpu_shared/CMakeLists.txt create mode 100644 backends/gpu_shared/README.md create mode 100644 backends/gpu_shared/runtime/SharedGpuContext.cpp create mode 100644 backends/gpu_shared/runtime/SharedGpuContext.h create mode 100644 backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp create mode 100644 backends/gpu_shared/runtime/SharedGpuContextRegistry.h create mode 100644 backends/gpu_shared/runtime/SharedGpuRuntimeConfig.cpp create mode 100644 backends/gpu_shared/runtime/SharedGpuRuntimeConfig.h create mode 100644 backends/gpu_shared/runtime/export.h create mode 100644 backends/gpu_shared/runtime/test/CMakeLists.txt create mode 100644 backends/gpu_shared/runtime/test/SharedGpuContextRegistryTest.cpp create mode 100644 backends/gpu_shared/runtime/test/SharedGpuRuntimeConfigTest.cpp create mode 100644 backends/gpu_shared/targets.bzl diff --git a/CMakeLists.txt b/CMakeLists.txt index 93e02c521eb..d8b4b4cb2c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -820,6 +820,9 @@ configure_file( install(FILES ${CMAKE_CURRENT_BINARY_DIR}/executorch-backend-dependencies.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ExecuTorch ) +if(EXECUTORCH_BUILD_VULKAN OR EXECUTORCH_BUILD_VGF) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/backends/gpu_shared) +endif() if(EXECUTORCH_BUILD_ARM_BAREMETAL OR EXECUTORCH_BUILD_ARM_ETHOSU_LINUX diff --git a/backends/gpu_shared/BUCK b/backends/gpu_shared/BUCK new file mode 100644 index 00000000000..18ea84d0bd7 --- /dev/null +++ b/backends/gpu_shared/BUCK @@ -0,0 +1,11 @@ +load( + "@fbcode_macros//build_defs:build_file_migration.bzl", + "fbcode_target", + "non_fbcode_target", +) +load(":targets.bzl", "define_common_targets") + +oncall("executorch") + +non_fbcode_target(_kind = define_common_targets,) +fbcode_target(_kind = define_common_targets,) diff --git a/backends/gpu_shared/CMakeLists.txt b/backends/gpu_shared/CMakeLists.txt new file mode 100644 index 00000000000..f1aa4bcdb97 --- /dev/null +++ b/backends/gpu_shared/CMakeLists.txt @@ -0,0 +1,93 @@ +# 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. + +cmake_minimum_required(VERSION 3.19) + +if(NOT EXECUTORCH_ROOT) + set(EXECUTORCH_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..) +endif() + +include(GNUInstallDirs) +find_package(Threads REQUIRED) + +set(GPU_SHARED_VULKAN_HEADERS_PATH + ${EXECUTORCH_ROOT}/backends/vulkan/third-party/Vulkan-Headers +) + +if(NOT EXISTS "${GPU_SHARED_VULKAN_HEADERS_PATH}/include/vulkan/vulkan.h") + message( + FATAL_ERROR + "The shared GPU runtime requires the vendored Vulkan-Headers submodule. " + "Run from the repository root:\n" " git submodule update --init " + "backends/vulkan/third-party/Vulkan-Headers" + ) +endif() + +# SHARED on purpose: VGF and Vulkan may be separate delegate DSOs, but they must +# observe one process-wide registry. A static copy in each DSO would create +# independent registries and defeat context sharing. +add_library( + executorch_gpu_shared_runtime SHARED + ${CMAKE_CURRENT_SOURCE_DIR}/runtime/SharedGpuContext.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/runtime/SharedGpuContextRegistry.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/runtime/SharedGpuRuntimeConfig.cpp +) +add_library(executorch::gpu_shared_runtime ALIAS executorch_gpu_shared_runtime) + +target_compile_definitions( + executorch_gpu_shared_runtime PRIVATE EXECUTORCH_GPU_SHARED_BUILDING +) + +target_include_directories( + executorch_gpu_shared_runtime + PUBLIC $ + $ + $ + $ +) + +target_link_libraries( + executorch_gpu_shared_runtime + PUBLIC executorch_core + PRIVATE Threads::Threads +) + +set_target_properties( + executorch_gpu_shared_runtime + PROPERTIES CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES +) + +install( + TARGETS executorch_gpu_shared_runtime + EXPORT ExecuTorchTargets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/runtime/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/executorch/backends/gpu_shared/runtime + FILES_MATCHING + PATTERN "*.h" + PATTERN "test" EXCLUDE +) + +# SharedGpuContext.h is a public installed header and includes +# . Install the same vendored headers used by the build so an +# installed ExecuTorch package does not depend on an unrelated system Vulkan +# SDK. +install(DIRECTORY ${GPU_SHARED_VULKAN_HEADERS_PATH}/include/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +if(EXECUTORCH_BUILD_TESTS) + add_subdirectory(runtime/test) +endif() diff --git a/backends/gpu_shared/README.md b/backends/gpu_shared/README.md new file mode 100644 index 00000000000..492cde153a0 --- /dev/null +++ b/backends/gpu_shared/README.md @@ -0,0 +1,61 @@ +# Shared GPU runtime + +This component is the backend-neutral runtime bridge used by the VGF and +ExecuTorch Vulkan delegates. It deliberately does not introduce a unified +partitioner or a wrapper delegate. + +The standard AOT flow remains explicit partitioner composition, with VGF +claiming supported regions first and Vulkan filling the remaining regions: + +```python +lowered = to_edge_transform_and_lower( + exported, + partitioner=[ + VgfPartitioner(vgf_compile_spec), + VulkanPartitioner(vulkan_compile_spec), + ], +) +``` + +## Runtime options + +Context selection is configured at model load time through `RuntimeSpec` / +`BackendOptions`, not through serialized `CompileSpec` values: + +| Key | Type | Default | Accepted values | +| --- | --- | --- | --- | +| `gpu_shared_context_token` | string | `default` | Any non-empty token | +| `gpu_shared_context_mode` | string | `lookup_or_create` | `disabled`, `lookup_only`, `lookup_or_create`, `create_only` | +| `gpu_shared_group_id` | int | `0` | Any `int` value | + +Both delegates must receive the same option values to resolve the same registry +key. + +## Ownership and validation + +`SharedGpuContext` carries Vulkan handles but never calls Vulkan entry points +itself. Every registered context must provide a non-null `lifetime_anchor` whose +lifetime guarantees that the Vulkan instance, physical device, device, and queue +remain valid until the final `SharedGpuContextPtr` is released. For a +backend-created context, the anchor can own the backend runtime and perform +teardown through that backend's Vulkan dispatch mechanism. For externally +created Vulkan objects, the application must provide an anchor whose ownership +keeps those objects alive for the same period. + +`unregister_context()` removes the context from the registry and prevents new +lookups; it does not revoke `SharedGpuContextPtr` instances already held by +delegates. Actual Vulkan teardown is therefore safe only after the final +outstanding context reference releases its `lifetime_anchor`. The registry +itself is intentionally process-lifetime; call `unregister_context()` to remove +registry discoverability before deterministic teardown. + +The `VkQueue` is shared process state and Vulkan queue operations require +external host synchronization. Consumers must issue queue operations through +`SharedGpuContext::with_locked_queue()` so independently initialized delegates +serialize access using the mutex stored in the shared context rather than +backend-local locks. + +The registrant also declares the device extensions enabled at `VkDevice` +creation. A consuming backend must check its required extensions with +`has_device_extension()` before using the context; Vulkan does not expose a +post-creation query for the list of extensions that were enabled. diff --git a/backends/gpu_shared/runtime/SharedGpuContext.cpp b/backends/gpu_shared/runtime/SharedGpuContext.cpp new file mode 100644 index 00000000000..81b8de51cb5 --- /dev/null +++ b/backends/gpu_shared/runtime/SharedGpuContext.cpp @@ -0,0 +1,63 @@ +/* + * 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 + +namespace executorch { +namespace backends { +namespace gpu_shared { + +SharedGpuContext::SharedGpuContext(SharedGpuContextCreateInfo create_info) + : create_info_(std::move(create_info)) {} + +SharedGpuContext::~SharedGpuContext() = default; + +const SharedGpuContextKey& SharedGpuContext::key() const { + return create_info_.key; +} + +VkInstance SharedGpuContext::instance() const { + return create_info_.instance; +} + +VkPhysicalDevice SharedGpuContext::physical_device() const { + return create_info_.physical_device; +} + +VkDevice SharedGpuContext::device() const { + return create_info_.device; +} + +uint32_t SharedGpuContext::queue_family_index() const { + return create_info_.queue_family_index; +} + +bool SharedGpuContext::has_device_extension( + std::string_view extension_name) const { + return std::any_of( + create_info_.enabled_device_extensions.begin(), + create_info_.enabled_device_extensions.end(), + [extension_name](const std::string& enabled_extension) { + return enabled_extension == extension_name; + }); +} + +bool SharedGpuContext::is_valid() const { + return create_info_.key.valid() && create_info_.instance != VK_NULL_HANDLE && + create_info_.physical_device != VK_NULL_HANDLE && + create_info_.device != VK_NULL_HANDLE && + create_info_.queue != VK_NULL_HANDLE && + create_info_.queue_family_index != std::numeric_limits::max() && + create_info_.lifetime_anchor != nullptr; +} + +} // namespace gpu_shared +} // namespace backends +} // namespace executorch diff --git a/backends/gpu_shared/runtime/SharedGpuContext.h b/backends/gpu_shared/runtime/SharedGpuContext.h new file mode 100644 index 00000000000..304a1a6c801 --- /dev/null +++ b/backends/gpu_shared/runtime/SharedGpuContext.h @@ -0,0 +1,100 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include + +namespace executorch { +namespace backends { +namespace gpu_shared { + +struct SharedGpuContextKey final { + std::string token; + int group_id = 0; + + bool valid() const { + return !token.empty(); + } + + friend bool operator==( + const SharedGpuContextKey& lhs, + const SharedGpuContextKey& rhs) { + return lhs.group_id == rhs.group_id && lhs.token == rhs.token; + } + + friend bool operator!=( + const SharedGpuContextKey& lhs, + const SharedGpuContextKey& rhs) { + return !(lhs == rhs); + } +}; + +// The shared layer carries Vulkan handles but deliberately does not call Vulkan +// entry points itself. Every registered context must provide a lifetime_anchor +// whose lifetime guarantees that instance, physical_device, device, and queue +// remain valid until the final SharedGpuContext reference is released. The +// anchor destructor may perform backend/application Vulkan teardown. +struct SharedGpuContextCreateInfo final { + SharedGpuContextKey key; + VkInstance instance = VK_NULL_HANDLE; + VkPhysicalDevice physical_device = VK_NULL_HANDLE; + VkDevice device = VK_NULL_HANDLE; + VkQueue queue = VK_NULL_HANDLE; + uint32_t queue_family_index = std::numeric_limits::max(); + std::vector enabled_device_extensions; + std::shared_ptr lifetime_anchor; +}; + +class EXECUTORCH_GPU_SHARED_API SharedGpuContext final { + public: + explicit SharedGpuContext(SharedGpuContextCreateInfo create_info); + ~SharedGpuContext(); + + SharedGpuContext(const SharedGpuContext&) = delete; + SharedGpuContext& operator=(const SharedGpuContext&) = delete; + + const SharedGpuContextKey& key() const; + VkInstance instance() const; + VkPhysicalDevice physical_device() const; + VkDevice device() const; + + // Vulkan queue operations require external host synchronization. All + // delegates sharing this context must issue queue operations through this + // callback so they synchronize on the same mutex. The VkQueue must not be + // retained and used after the callback returns. + template + decltype(auto) with_locked_queue(Fn&& fn) const { + std::lock_guard lock(queue_mutex_); + return std::forward(fn)(create_info_.queue); + } + + uint32_t queue_family_index() const; + bool has_device_extension(std::string_view extension_name) const; + bool is_valid() const; + + private: + SharedGpuContextCreateInfo create_info_; + mutable std::mutex queue_mutex_; +}; + +using SharedGpuContextPtr = std::shared_ptr; + +} // namespace gpu_shared +} // namespace backends +} // namespace executorch diff --git a/backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp b/backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp new file mode 100644 index 00000000000..6e10f09e668 --- /dev/null +++ b/backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp @@ -0,0 +1,209 @@ +/* + * 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 + +namespace executorch { +namespace backends { +namespace gpu_shared { + +SharedGpuContextRegistry& SharedGpuContextRegistry::Get() { + // The default context is process-persistent. Intentionally do not register a + // static destructor: delegate DSOs may be unloaded before their lifetime + // anchors, so teardown must be explicit through unregister_context(). + static auto* registry = new SharedGpuContextRegistry(); + return *registry; +} + +size_t SharedGpuContextRegistry::KeyHash::operator()( + const SharedGpuContextKey& key) const { + const size_t token_hash = std::hash{}(key.token); + const size_t group_hash = std::hash{}(key.group_id); + return token_hash ^ + (group_hash + static_cast(0x9e3779b9) + (token_hash << 6) + + (token_hash >> 2)); +} + +SharedGpuContextPtr SharedGpuContextRegistry::lookup( + const SharedGpuContextKey& key) { + if (!key.valid()) { + return nullptr; + } + + SharedGpuContextPtr stale_context; + { + std::lock_guard lock(mutex_); + auto it = registry_.find(key); + if (it == registry_.end()) { + return nullptr; + } + + const auto& entry = it->second; + if (entry->context && entry->context->is_valid()) { + return entry->context; + } + + // Move any stale context out while holding the registry lock, but release + // it only after unlocking: its lifetime_anchor may run backend teardown and + // re-enter this registry. + stale_context = std::move(entry->context); + if (!entry->creating) { + registry_.erase(it); + } + } + + stale_context.reset(); + return nullptr; +} + +runtime::Result SharedGpuContextRegistry::lookup_or_create( + const SharedGpuContextKey& key, + CreateFn create_fn) { + if (!key.valid() || !create_fn) { + return runtime::Error::InvalidArgument; + } + + std::shared_ptr entry; + { + std::unique_lock lock(mutex_); + auto [it, inserted] = registry_.try_emplace(key, std::make_shared()); + (void)inserted; + entry = it->second; + + while (entry->creating) { + entry->creation_complete.wait(lock); + } + + if (entry->context && entry->context->is_valid()) { + return entry->context; + } + + entry->context.reset(); + entry->creating = true; + } + + auto maybe_created = create_fn(); + runtime::Error create_error = runtime::Error::Ok; + SharedGpuContextPtr created; + if (!maybe_created.ok()) { + create_error = maybe_created.error(); + } else { + created = maybe_created.get(); + if (!created || !created->is_valid() || created->key() != key) { + created.reset(); + create_error = runtime::Error::InvalidArgument; + } + } + + SharedGpuContextPtr selected; + { + std::lock_guard lock(mutex_); + + // register_context() is allowed to win a race with a creator. In that + // case, discard the newly created context and return the registered one. + if (entry->context && entry->context->is_valid()) { + selected = entry->context; + } else if (created) { + entry->context = std::move(created); + selected = entry->context; + } + + entry->creating = false; + entry->creation_complete.notify_all(); + } + + if (selected) { + return selected; + } + return create_error == runtime::Error::Ok ? runtime::Error::Internal + : create_error; +} + +runtime::Error SharedGpuContextRegistry::register_context( + SharedGpuContextPtr context) { + if (!context || !context->is_valid()) { + return runtime::Error::InvalidArgument; + } + + std::lock_guard lock(mutex_); + auto [it, inserted] = + registry_.try_emplace(context->key(), std::make_shared()); + (void)inserted; + auto& entry = it->second; + + if (entry->context && entry->context->is_valid()) { + return entry->context.get() == context.get() + ? runtime::Error::Ok + : runtime::Error::AlreadyLoaded; + } + + entry->context = std::move(context); + entry->creation_complete.notify_all(); + return runtime::Error::Ok; +} + +runtime::Result +SharedGpuContextRegistry::register_external_context( + SharedGpuContextCreateInfo create_info) { + auto context = std::make_shared(std::move(create_info)); + const runtime::Error error = register_context(context); + if (error != runtime::Error::Ok) { + return error; + } + return context; +} + +runtime::Error SharedGpuContextRegistry::unregister_context( + const SharedGpuContextKey& key) { + if (!key.valid()) { + return runtime::Error::InvalidArgument; + } + + std::shared_ptr removed_entry; + { + std::lock_guard lock(mutex_); + auto it = registry_.find(key); + if (it == registry_.end()) { + return runtime::Error::NotFound; + } + if (it->second->creating) { + return runtime::Error::InvalidState; + } + + // Unlink the entry under the registry lock, but retain ownership locally so + // SharedGpuContext/lifetime_anchor destruction cannot run while mutex_ is + // held. Backend teardown is allowed to re-enter this registry. + removed_entry = std::move(it->second); + registry_.erase(it); + } + + removed_entry.reset(); + return runtime::Error::Ok; +} + +void SharedGpuContextRegistry::clear_for_testing() { + decltype(registry_) removed_entries; + { + std::lock_guard lock(mutex_); + for (const auto& item : registry_) { + item.second->creating = false; + item.second->creation_complete.notify_all(); + } + + // Remove everything atomically from the live registry, then allow entries + // (and their lifetime anchors) to be destroyed after mutex_ is released. + removed_entries.swap(registry_); + } + + removed_entries.clear(); +} + +} // namespace gpu_shared +} // namespace backends +} // namespace executorch diff --git a/backends/gpu_shared/runtime/SharedGpuContextRegistry.h b/backends/gpu_shared/runtime/SharedGpuContextRegistry.h new file mode 100644 index 00000000000..8efc5e16d5e --- /dev/null +++ b/backends/gpu_shared/runtime/SharedGpuContextRegistry.h @@ -0,0 +1,74 @@ +/* + * 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 + +#include +#include +#include +#include +#include + +namespace executorch { +namespace backends { +namespace gpu_shared { + +// Process-local registry used by independently initialized GPU delegates. The +// canonical target is a shared library so all delegate DSOs observe the same +// registry instance. +class EXECUTORCH_GPU_SHARED_API SharedGpuContextRegistry final { + public: + using CreateFn = std::function()>; + + static SharedGpuContextRegistry& Get(); + + SharedGpuContextRegistry(const SharedGpuContextRegistry&) = delete; + SharedGpuContextRegistry& operator=(const SharedGpuContextRegistry&) = delete; + + SharedGpuContextPtr lookup(const SharedGpuContextKey& key); + + runtime::Result lookup_or_create( + const SharedGpuContextKey& key, + CreateFn create_fn); + + runtime::Error register_context(SharedGpuContextPtr context); + + runtime::Result register_external_context( + SharedGpuContextCreateInfo create_info); + + runtime::Error unregister_context(const SharedGpuContextKey& key); + + void clear_for_testing(); + + private: + struct Entry final { + SharedGpuContextPtr context; + bool creating = false; + std::condition_variable creation_complete; + }; + + struct KeyHash final { + size_t operator()(const SharedGpuContextKey& key) const; + }; + + SharedGpuContextRegistry() = default; + + // Never release a SharedGpuContext/lifetime_anchor while this mutex is held: + // backend teardown may re-enter the registry. + std::mutex mutex_; + std::unordered_map, KeyHash> + registry_; +}; + +} // namespace gpu_shared +} // namespace backends +} // namespace executorch diff --git a/backends/gpu_shared/runtime/SharedGpuRuntimeConfig.cpp b/backends/gpu_shared/runtime/SharedGpuRuntimeConfig.cpp new file mode 100644 index 00000000000..0a6603086c6 --- /dev/null +++ b/backends/gpu_shared/runtime/SharedGpuRuntimeConfig.cpp @@ -0,0 +1,79 @@ +/* + * 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 + +namespace executorch { +namespace backends { +namespace gpu_shared { +namespace { + +runtime::Result parse_context_mode(const char* value) { + if (value == nullptr) { + return runtime::Error::InvalidArgument; + } + if (std::strcmp(value, "disabled") == 0) { + return SharedContextMode::kDisabled; + } + if (std::strcmp(value, "lookup_only") == 0) { + return SharedContextMode::kLookupOnly; + } + if (std::strcmp(value, "lookup_or_create") == 0) { + return SharedContextMode::kLookupOrCreate; + } + if (std::strcmp(value, "create_only") == 0) { + return SharedContextMode::kCreateOnly; + } + return runtime::Error::InvalidArgument; +} + +} // namespace + +// This API is consumed by the Vulkan/VGF delegate integration follow-up PRs. +// The phase-2 runtime library intentionally has no production caller yet. +// cppcheck-suppress unusedFunction +runtime::Result parse_shared_gpu_runtime_config( + const runtime::BackendInitContext& context) { + SharedGpuRuntimeConfig config; + + auto token = context.get_runtime_spec(kSharedContextTokenOption); + if (token.ok()) { + config.token = token.get(); + } else if (token.error() != runtime::Error::NotFound) { + return token.error(); + } + + auto mode = context.get_runtime_spec(kSharedContextModeOption); + if (mode.ok()) { + auto parsed_mode = parse_context_mode(mode.get()); + if (!parsed_mode.ok()) { + return parsed_mode.error(); + } + config.context_mode = parsed_mode.get(); + } else if (mode.error() != runtime::Error::NotFound) { + return mode.error(); + } + + auto group_id = context.get_runtime_spec(kSharedGroupIdOption); + if (group_id.ok()) { + config.group_id = group_id.get(); + } else if (group_id.error() != runtime::Error::NotFound) { + return group_id.error(); + } + + if (config.enabled() && config.token.empty()) { + return runtime::Error::InvalidArgument; + } + + return config; +} + +} // namespace gpu_shared +} // namespace backends +} // namespace executorch diff --git a/backends/gpu_shared/runtime/SharedGpuRuntimeConfig.h b/backends/gpu_shared/runtime/SharedGpuRuntimeConfig.h new file mode 100644 index 00000000000..b80dbd32c23 --- /dev/null +++ b/backends/gpu_shared/runtime/SharedGpuRuntimeConfig.h @@ -0,0 +1,62 @@ +/* + * 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 +#include + +namespace executorch { +namespace backends { +namespace gpu_shared { + +inline constexpr char kSharedContextTokenOption[] = "gpu_shared_context_token"; +inline constexpr char kSharedContextModeOption[] = "gpu_shared_context_mode"; +inline constexpr char kSharedGroupIdOption[] = "gpu_shared_group_id"; + +enum class SharedContextMode : uint8_t { + kDisabled = 0, + kLookupOnly = 1, + kLookupOrCreate = 2, + kCreateOnly = 3, +}; + +// Load-time configuration shared by the VGF and Vulkan delegates. These values +// are RuntimeSpec options: context selection is a deployment concern and must +// not be serialized into a backend CompileSpec or a .pte file. +struct SharedGpuRuntimeConfig final { + std::string token = "default"; + int group_id = 0; + SharedContextMode context_mode = SharedContextMode::kLookupOrCreate; + + bool enabled() const { + return context_mode != SharedContextMode::kDisabled; + } + + bool lookup_only() const { + return context_mode == SharedContextMode::kLookupOnly; + } + + bool lookup_or_create() const { + return context_mode == SharedContextMode::kLookupOrCreate; + } + + bool create_only() const { + return context_mode == SharedContextMode::kCreateOnly; + } +}; + +EXECUTORCH_GPU_SHARED_API runtime::Result +parse_shared_gpu_runtime_config(const runtime::BackendInitContext& context); + +} // namespace gpu_shared +} // namespace backends +} // namespace executorch diff --git a/backends/gpu_shared/runtime/export.h b/backends/gpu_shared/runtime/export.h new file mode 100644 index 00000000000..4b8385766b5 --- /dev/null +++ b/backends/gpu_shared/runtime/export.h @@ -0,0 +1,21 @@ +/* + * 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 + +// The shared GPU registry must have one definition across all delegate shared +// objects in a process. Export its public API from +// executorch_gpu_shared_runtime. +#if defined(_WIN32) +#if defined(EXECUTORCH_GPU_SHARED_BUILDING) +#define EXECUTORCH_GPU_SHARED_API __declspec(dllexport) +#else +#define EXECUTORCH_GPU_SHARED_API __declspec(dllimport) +#endif +#else +#define EXECUTORCH_GPU_SHARED_API __attribute__((visibility("default"))) +#endif diff --git a/backends/gpu_shared/runtime/test/CMakeLists.txt b/backends/gpu_shared/runtime/test/CMakeLists.txt new file mode 100644 index 00000000000..ee0c78eb0c0 --- /dev/null +++ b/backends/gpu_shared/runtime/test/CMakeLists.txt @@ -0,0 +1,23 @@ +# 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. + +add_executable( + shared_gpu_runtime_test + ${CMAKE_CURRENT_SOURCE_DIR}/SharedGpuContextRegistryTest.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/SharedGpuRuntimeConfigTest.cpp +) + +target_link_libraries( + shared_gpu_runtime_test PRIVATE executorch_gpu_shared_runtime + Threads::Threads +) + +if(TARGET GTest::gtest_main) + target_link_libraries(shared_gpu_runtime_test PRIVATE GTest::gtest_main) +else() + target_link_libraries(shared_gpu_runtime_test PRIVATE gtest gtest_main) +endif() + +add_test(NAME shared_gpu_runtime_test COMMAND shared_gpu_runtime_test) diff --git a/backends/gpu_shared/runtime/test/SharedGpuContextRegistryTest.cpp b/backends/gpu_shared/runtime/test/SharedGpuContextRegistryTest.cpp new file mode 100644 index 00000000000..480ba03d468 --- /dev/null +++ b/backends/gpu_shared/runtime/test/SharedGpuContextRegistryTest.cpp @@ -0,0 +1,414 @@ +/* + * 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 +// Cppcheck's lint environment may not expand the gtest macros. +#ifndef TEST +#define TEST(test_suite_name, test_name) void test_suite_name##_##test_name() +#endif +#ifndef TEST_F +#define TEST_F(test_fixture, test_name) void test_fixture##_##test_name() +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using executorch::backends::gpu_shared::SharedGpuContext; +using executorch::backends::gpu_shared::SharedGpuContextCreateInfo; +using executorch::backends::gpu_shared::SharedGpuContextKey; +using executorch::backends::gpu_shared::SharedGpuContextPtr; +using executorch::backends::gpu_shared::SharedGpuContextRegistry; +using executorch::runtime::Error; +using executorch::runtime::Result; + +namespace { + +template +Handle fake_handle(uintptr_t value) { + return reinterpret_cast(value); +} + +SharedGpuContextCreateInfo make_create_info( + SharedGpuContextKey key, + uintptr_t handle_base = 1) { + SharedGpuContextCreateInfo info; + info.key = std::move(key); + info.instance = fake_handle(handle_base); + info.physical_device = fake_handle(handle_base + 1); + info.device = fake_handle(handle_base + 2); + info.queue = fake_handle(handle_base + 3); + info.queue_family_index = 4; + // Tests use a dummy owner by default now that every valid context requires a + // lifetime anchor. Tests that exercise ownership replace this anchor. + info.lifetime_anchor = std::make_shared(0); + return info; +} + +struct RegistryLookupProbeState final { + std::mutex mutex; + std::condition_variable cv; + bool complete = false; +}; + +class RegistryLookupLifetimeAnchor final { + public: + RegistryLookupLifetimeAnchor( + SharedGpuContextKey lookup_key, + std::shared_ptr state, + std::atomic* lookup_completed_during_destruction) + : lookup_key_(std::move(lookup_key)), + state_(std::move(state)), + lookup_completed_during_destruction_( + lookup_completed_during_destruction) {} + + ~RegistryLookupLifetimeAnchor() { + // Probe the registry from another thread. If this destructor runs while the + // registry mutex is held, lookup() cannot complete until destruction + // returns and unregister_context() releases the mutex. The timeout prevents + // the regression test itself from deadlocking on the buggy implementation. + std::thread([lookup_key = lookup_key_, state = state_]() { + (void)SharedGpuContextRegistry::Get().lookup(lookup_key); + { + std::lock_guard lock(state->mutex); + state->complete = true; + } + state->cv.notify_all(); + }).detach(); + + std::unique_lock lock(state_->mutex); + const bool completed = + state_->cv.wait_for(lock, std::chrono::seconds(1), [state = state_]() { + return state->complete; + }); + lookup_completed_during_destruction_->store(completed); + } + + private: + SharedGpuContextKey lookup_key_; + std::shared_ptr state_; + std::atomic* lookup_completed_during_destruction_; +}; + +class SharedGpuContextRegistryTest : public ::testing::Test { + protected: + void SetUp() override { + SharedGpuContextRegistry::Get().clear_for_testing(); + } + + void TearDown() override { + SharedGpuContextRegistry::Get().clear_for_testing(); + } +}; + +// cppcheck-suppress unusedFunction +TEST_F(SharedGpuContextRegistryTest, RegistryOwnsPersistentContext) { + const SharedGpuContextKey key{"scene0", 3}; + std::weak_ptr owner_weak; + + { + auto owner = std::make_shared(17); + owner_weak = owner; + auto info = make_create_info(key); + info.lifetime_anchor = owner; + + auto registered = SharedGpuContextRegistry::Get().register_external_context( + std::move(info)); + ASSERT_TRUE(registered.ok()); + } + + EXPECT_FALSE(owner_weak.expired()); + EXPECT_NE(SharedGpuContextRegistry::Get().lookup(key), nullptr); + EXPECT_EQ(SharedGpuContextRegistry::Get().unregister_context(key), Error::Ok); + EXPECT_TRUE(owner_weak.expired()); +} + +// cppcheck-suppress unusedFunction +TEST_F( + SharedGpuContextRegistryTest, + UnregisterDestroysLifetimeAnchorOutsideRegistryLock) { + const SharedGpuContextKey key{"scene0", 9}; + const SharedGpuContextKey probe_key{"probe", 9}; + std::atomic lookup_completed_during_destruction{false}; + auto probe_state = std::make_shared(); + + { + auto info = make_create_info(key); + info.lifetime_anchor = std::make_shared( + probe_key, probe_state, &lookup_completed_during_destruction); + + auto registered = SharedGpuContextRegistry::Get().register_external_context( + std::move(info)); + ASSERT_TRUE(registered.ok()); + } + + // The registry is now the only owner of the SharedGpuContext. Unregistering + // therefore destroys its lifetime anchor. The probe must be able to acquire + // the registry mutex before that destruction returns. + EXPECT_EQ(SharedGpuContextRegistry::Get().unregister_context(key), Error::Ok); + EXPECT_TRUE(lookup_completed_during_destruction.load()); + + // On a broken implementation the probe only completes after unregister has + // released the mutex. Wait for it here so the detached thread cannot escape + // the test and race fixture teardown. + std::unique_lock lock(probe_state->mutex); + EXPECT_TRUE( + probe_state->cv.wait_for(lock, std::chrono::seconds(1), [probe_state]() { + return probe_state->complete; + })); +} + +// cppcheck-suppress unusedFunction +TEST_F(SharedGpuContextRegistryTest, LookupOrCreateRunsCreatorOnce) { + const SharedGpuContextKey key{"scene0", 4}; + std::atomic create_count{0}; + std::mutex creator_mutex; + std::condition_variable creator_cv; + bool creator_entered = false; + bool allow_creator_to_finish = false; + + auto create_fn = [&]() -> Result { + ++create_count; + { + std::unique_lock lock(creator_mutex); + creator_entered = true; + creator_cv.notify_all(); + creator_cv.wait(lock, [&]() { return allow_creator_to_finish; }); + } + return std::make_shared(make_create_info(key)); + }; + + constexpr size_t kThreadCount = 8; + std::vector results(kThreadCount); + std::vector errors(kThreadCount, Error::Internal); + std::vector threads; + threads.reserve(kThreadCount); + for (size_t i = 0; i < kThreadCount; ++i) { + threads.emplace_back([&, i]() { + auto result = + SharedGpuContextRegistry::Get().lookup_or_create(key, create_fn); + errors[i] = result.error(); + if (result.ok()) { + results[i] = result.get(); + } + }); + } + + { + std::unique_lock lock(creator_mutex); + creator_cv.wait(lock, [&]() { return creator_entered; }); + allow_creator_to_finish = true; + } + creator_cv.notify_all(); + + for (auto& thread : threads) { + thread.join(); + } + + EXPECT_EQ(create_count.load(), 1); + for (size_t i = 0; i < kThreadCount; ++i) { + EXPECT_EQ(errors[i], Error::Ok); + EXPECT_EQ(results[i], results[0]); + } +} + +// cppcheck-suppress unusedFunction +TEST_F(SharedGpuContextRegistryTest, RejectsDifferentDuplicateContext) { + const SharedGpuContextKey key{"scene0", 5}; + auto first = std::make_shared(make_create_info(key, 10)); + auto second = std::make_shared(make_create_info(key, 20)); + + EXPECT_EQ(SharedGpuContextRegistry::Get().register_context(first), Error::Ok); + EXPECT_EQ( + SharedGpuContextRegistry::Get().register_context(second), + Error::AlreadyLoaded); + EXPECT_EQ(SharedGpuContextRegistry::Get().lookup(key), first); +} + +// cppcheck-suppress unusedFunction +TEST_F(SharedGpuContextRegistryTest, ValidatesContextIdentity) { + const SharedGpuContextKey requested_key{"scene0", 6}; + const SharedGpuContextKey returned_key{"other", 6}; + + auto result = SharedGpuContextRegistry::Get().lookup_or_create( + requested_key, [&]() -> Result { + return std::make_shared( + make_create_info(returned_key)); + }); + + ASSERT_FALSE(result.ok()); + EXPECT_EQ(result.error(), Error::InvalidArgument); + EXPECT_EQ(SharedGpuContextRegistry::Get().lookup(requested_key), nullptr); +} + +// cppcheck-suppress unusedFunction +TEST_F(SharedGpuContextRegistryTest, ReportsDeclaredDeviceExtensions) { + const SharedGpuContextKey key{"scene0", 7}; + auto info = make_create_info(key); + info.enabled_device_extensions = {"VK_ARM_tensors", "VK_ARM_data_graph"}; + SharedGpuContext context(std::move(info)); + + EXPECT_TRUE(context.has_device_extension("VK_ARM_tensors")); + EXPECT_TRUE(context.has_device_extension("VK_ARM_data_graph")); + EXPECT_FALSE(context.has_device_extension("VK_KHR_nonexistent")); +} + +// cppcheck-suppress unusedFunction +TEST_F(SharedGpuContextRegistryTest, RejectsContextWithoutLifetimeAnchor) { + const SharedGpuContextKey key{"scene0", 10}; + auto info = make_create_info(key); + info.lifetime_anchor.reset(); + + auto result = SharedGpuContextRegistry::Get().register_external_context( + std::move(info)); + + ASSERT_FALSE(result.ok()); + EXPECT_EQ(result.error(), Error::InvalidArgument); +} + +// cppcheck-suppress unusedFunction +TEST_F( + SharedGpuContextRegistryTest, + UnregisterKeepsLifetimeAnchorAliveWhileContextIsReferenced) { + const SharedGpuContextKey key{"scene0", 11}; + std::weak_ptr owner_weak; + SharedGpuContextPtr held_context; + + { + auto owner = std::make_shared(17); + owner_weak = owner; + + auto info = make_create_info(key); + info.lifetime_anchor = owner; + + auto registered = SharedGpuContextRegistry::Get().register_external_context( + std::move(info)); + ASSERT_TRUE(registered.ok()); + + held_context = SharedGpuContextRegistry::Get().lookup(key); + ASSERT_NE(held_context, nullptr); + } + + EXPECT_FALSE(owner_weak.expired()); + EXPECT_EQ(SharedGpuContextRegistry::Get().unregister_context(key), Error::Ok); + EXPECT_EQ(SharedGpuContextRegistry::Get().lookup(key), nullptr); + + // unregister_context() removes registry ownership only. An existing delegate + // reference must continue to keep the underlying Vulkan objects alive. + EXPECT_FALSE(owner_weak.expired()); + + held_context.reset(); + EXPECT_TRUE(owner_weak.expired()); +} + +// cppcheck-suppress unusedFunction +TEST_F(SharedGpuContextRegistryTest, SerializesSharedQueueAccess) { + const SharedGpuContextKey key{"scene0", 12}; + auto context = std::make_shared(make_create_info(key)); + + std::mutex state_mutex; + std::condition_variable state_cv; + bool first_entered = false; + bool release_first = false; + bool second_started = false; + bool second_entered = false; + + std::thread first([&]() { + context->with_locked_queue([&](VkQueue) { + std::unique_lock lock(state_mutex); + first_entered = true; + state_cv.notify_all(); + state_cv.wait(lock, [&]() { return release_first; }); + }); + }); + + bool first_ready = false; + { + std::unique_lock lock(state_mutex); + first_ready = state_cv.wait_for( + lock, std::chrono::seconds(1), [&]() { return first_entered; }); + } + EXPECT_TRUE(first_ready); + if (!first_ready) { + { + std::lock_guard lock(state_mutex); + release_first = true; + } + state_cv.notify_all(); + first.join(); + return; + } + + std::thread second([&]() { + { + std::lock_guard lock(state_mutex); + second_started = true; + } + state_cv.notify_all(); + + context->with_locked_queue([&](VkQueue) { + { + std::lock_guard lock(state_mutex); + second_entered = true; + } + state_cv.notify_all(); + }); + }); + + bool second_ready = false; + bool entered_while_first_held_queue_lock = false; + { + std::unique_lock lock(state_mutex); + second_ready = state_cv.wait_for( + lock, std::chrono::seconds(1), [&]() { return second_started; }); + if (second_ready) { + entered_while_first_held_queue_lock = + state_cv.wait_for(lock, std::chrono::milliseconds(100), [&]() { + return second_entered; + }); + } + + release_first = true; + } + state_cv.notify_all(); + + EXPECT_TRUE(second_ready); + EXPECT_FALSE(entered_while_first_held_queue_lock); + + bool second_completed = false; + { + std::unique_lock lock(state_mutex); + second_completed = state_cv.wait_for( + lock, std::chrono::seconds(1), [&]() { return second_entered; }); + } + EXPECT_TRUE(second_completed); + + first.join(); + second.join(); +} + +// cppcheck-suppress unusedFunction +TEST_F(SharedGpuContextRegistryTest, RejectsIncompleteExternalContext) { + SharedGpuContextCreateInfo info; + info.key = {"scene0", 8}; + + auto result = SharedGpuContextRegistry::Get().register_external_context( + std::move(info)); + + ASSERT_FALSE(result.ok()); + EXPECT_EQ(result.error(), Error::InvalidArgument); +} + +} // namespace diff --git a/backends/gpu_shared/runtime/test/SharedGpuRuntimeConfigTest.cpp b/backends/gpu_shared/runtime/test/SharedGpuRuntimeConfigTest.cpp new file mode 100644 index 00000000000..a8a55e6b974 --- /dev/null +++ b/backends/gpu_shared/runtime/test/SharedGpuRuntimeConfigTest.cpp @@ -0,0 +1,117 @@ +/* + * 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 +// Cppcheck's lint environment may not expand the gtest macros. +#ifndef TEST +#define TEST(test_suite_name, test_name) void test_suite_name##_##test_name() +#endif + +using executorch::backends::gpu_shared::kSharedContextModeOption; +using executorch::backends::gpu_shared::kSharedContextTokenOption; +using executorch::backends::gpu_shared::kSharedGroupIdOption; +using executorch::backends::gpu_shared::parse_shared_gpu_runtime_config; +using executorch::backends::gpu_shared::SharedContextMode; +using executorch::runtime::BackendInitContext; +using executorch::runtime::BackendOption; +using executorch::runtime::BackendOptions; +using executorch::runtime::Error; +using executorch::runtime::Span; + +namespace { + +template +BackendInitContext make_context(BackendOptions& options) { + auto view = options.view(); + Span specs(view.data(), view.size()); + return BackendInitContext(nullptr, nullptr, nullptr, nullptr, specs); +} + +// cppcheck-suppress unusedFunction +TEST(SharedGpuRuntimeConfigTest, UsesPersistentSharedDefaults) { + BackendInitContext context(nullptr); + + auto result = parse_shared_gpu_runtime_config(context); + + ASSERT_TRUE(result.ok()); + EXPECT_EQ(result->token, "default"); + EXPECT_EQ(result->group_id, 0); + EXPECT_EQ(result->context_mode, SharedContextMode::kLookupOrCreate); +} + +// cppcheck-suppress unusedFunction +TEST(SharedGpuRuntimeConfigTest, ParsesRuntimeOptions) { + BackendOptions<3> options; + ASSERT_EQ(options.set_option(kSharedContextTokenOption, "scene0"), Error::Ok); + ASSERT_EQ( + options.set_option(kSharedContextModeOption, "lookup_only"), Error::Ok); + ASSERT_EQ(options.set_option(kSharedGroupIdOption, 7), Error::Ok); + auto context = make_context(options); + + auto result = parse_shared_gpu_runtime_config(context); + + ASSERT_TRUE(result.ok()); + EXPECT_EQ(result->token, "scene0"); + EXPECT_EQ(result->group_id, 7); + EXPECT_TRUE(result->lookup_only()); +} + +// cppcheck-suppress unusedFunction +TEST(SharedGpuRuntimeConfigTest, ParsesDisabledMode) { + BackendOptions<1> options; + ASSERT_EQ( + options.set_option(kSharedContextModeOption, "disabled"), Error::Ok); + auto context = make_context(options); + + auto result = parse_shared_gpu_runtime_config(context); + + ASSERT_TRUE(result.ok()); + EXPECT_FALSE(result->enabled()); +} + +// cppcheck-suppress unusedFunction +TEST(SharedGpuRuntimeConfigTest, RejectsUnknownMode) { + BackendOptions<1> options; + ASSERT_EQ( + options.set_option(kSharedContextModeOption, "automatic"), Error::Ok); + auto context = make_context(options); + + auto result = parse_shared_gpu_runtime_config(context); + + ASSERT_FALSE(result.ok()); + EXPECT_EQ(result.error(), Error::InvalidArgument); +} + +// cppcheck-suppress unusedFunction +TEST(SharedGpuRuntimeConfigTest, RejectsWrongRuntimeOptionType) { + BackendOptions<1> options; + ASSERT_EQ(options.set_option(kSharedGroupIdOption, "seven"), Error::Ok); + auto context = make_context(options); + + auto result = parse_shared_gpu_runtime_config(context); + + ASSERT_FALSE(result.ok()); + EXPECT_EQ(result.error(), Error::InvalidArgument); +} + +// cppcheck-suppress unusedFunction +TEST(SharedGpuRuntimeConfigTest, RejectsEmptyTokenWhenEnabled) { + BackendOptions<1> options; + ASSERT_EQ(options.set_option(kSharedContextTokenOption, ""), Error::Ok); + auto context = make_context(options); + + auto result = parse_shared_gpu_runtime_config(context); + + ASSERT_FALSE(result.ok()); + EXPECT_EQ(result.error(), Error::InvalidArgument); +} + +} // namespace diff --git a/backends/gpu_shared/targets.bzl b/backends/gpu_shared/targets.bzl new file mode 100644 index 00000000000..05d5769c829 --- /dev/null +++ b/backends/gpu_shared/targets.bzl @@ -0,0 +1,51 @@ +load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime") + + +def define_common_targets(): + """Defines the shared GPU runtime and its unit tests.""" + + # This target must be shareable rather than force-static. VGF and Vulkan can + # live in separate DSOs, but both must resolve the same process registry. + runtime.cxx_library( + name = "runtime", + srcs = [ + "runtime/SharedGpuContext.cpp", + "runtime/SharedGpuContextRegistry.cpp", + "runtime/SharedGpuRuntimeConfig.cpp", + ], + exported_headers = [ + "runtime/SharedGpuContext.h", + "runtime/SharedGpuContextRegistry.h", + "runtime/SharedGpuRuntimeConfig.h", + "runtime/export.h", + ], + force_static = False, + preprocessor_flags = [ + "-DEXECUTORCH_GPU_SHARED_BUILDING", + ], + visibility = ["PUBLIC"], + exported_deps = [ + "//executorch/runtime/backend:interface", + "//executorch/runtime/core:core", + "fbsource//third-party/khronos:vulkan-headers", + ], + ) + + runtime.cxx_test( + name = "shared_gpu_runtime_config_test", + srcs = ["runtime/test/SharedGpuRuntimeConfigTest.cpp"], + deps = [ + ":runtime", + "//executorch/runtime/backend:interface", + "//executorch/runtime/core:core", + ], + ) + + runtime.cxx_test( + name = "shared_gpu_context_registry_test", + srcs = ["runtime/test/SharedGpuContextRegistryTest.cpp"], + deps = [ + ":runtime", + "//executorch/runtime/core:core", + ], + ) From d6221d27b0220ccd72506114f64906e7a1b898cf Mon Sep 17 00:00:00 2001 From: Elena Zhelezina Date: Fri, 18 Sep 2026 16:37:19 +0100 Subject: [PATCH 2/6] Arm backend: Addressed comments. Change-Id: I0510a735c7ac56a2728b7e03b37b125a7d3524d7 --- backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp | 7 ++++++- backends/gpu_shared/runtime/SharedGpuContextRegistry.h | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp b/backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp index 6e10f09e668..a8d3037169a 100644 --- a/backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp +++ b/backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp @@ -70,6 +70,8 @@ runtime::Result SharedGpuContextRegistry::lookup_or_create( } std::shared_ptr entry; + SharedGpuContextPtr stale_context; + { std::unique_lock lock(mutex_); auto [it, inserted] = registry_.try_emplace(key, std::make_shared()); @@ -84,10 +86,13 @@ runtime::Result SharedGpuContextRegistry::lookup_or_create( return entry->context; } - entry->context.reset(); + stale_context = std::move(entry->context); entry->creating = true; } + // lifetime_anchor destruction may re-enter the registry. + stale_context.reset(); + auto maybe_created = create_fn(); runtime::Error create_error = runtime::Error::Ok; SharedGpuContextPtr created; diff --git a/backends/gpu_shared/runtime/SharedGpuContextRegistry.h b/backends/gpu_shared/runtime/SharedGpuContextRegistry.h index 8efc5e16d5e..1fbbed78646 100644 --- a/backends/gpu_shared/runtime/SharedGpuContextRegistry.h +++ b/backends/gpu_shared/runtime/SharedGpuContextRegistry.h @@ -47,6 +47,8 @@ class EXECUTORCH_GPU_SHARED_API SharedGpuContextRegistry final { runtime::Error unregister_context(const SharedGpuContextKey& key); + // Test-only. The caller must ensure that there are no concurrent + // registry operations or in-flight lookup_or_create() calls. void clear_for_testing(); private: From 61db1b4a64c2d59babf161d1d1eae77f1a582b0f Mon Sep 17 00:00:00 2001 From: Elena Zhelezina Date: Mon, 21 Sep 2026 17:13:20 +0100 Subject: [PATCH 3/6] Arm backend: Renamed to VulkanShared everything. Change-Id: I5f1afdc43c783901c5a8d9f7bdcd0f04674c51eb --- CMakeLists.txt | 2 +- .../runtime/SharedGpuContextRegistry.h | 76 --------- backends/gpu_shared/runtime/export.h | 21 --- backends/{gpu_shared => vulkan_shared}/BUCK | 0 .../CMakeLists.txt | 40 ++--- .../{gpu_shared => vulkan_shared}/README.md | 16 +- .../runtime/SharedVulkanContext.cpp} | 25 +-- .../runtime/SharedVulkanContext.h} | 44 +++--- .../runtime/SharedVulkanContextRegistry.cpp} | 55 +++---- .../runtime/SharedVulkanContextRegistry.h | 77 +++++++++ .../runtime/SharedVulkanRuntimeConfig.cpp} | 23 +-- .../runtime/SharedVulkanRuntimeConfig.h} | 20 +-- backends/vulkan_shared/runtime/export.h | 21 +++ .../runtime/test/CMakeLists.txt | 6 +- .../test/SharedVulkanContextRegistryTest.cpp} | 148 +++++++++--------- .../test/SharedVulkanRuntimeConfigTest.cpp} | 44 +++--- .../{gpu_shared => vulkan_shared}/targets.bzl | 20 +-- 17 files changed, 327 insertions(+), 311 deletions(-) delete mode 100644 backends/gpu_shared/runtime/SharedGpuContextRegistry.h delete mode 100644 backends/gpu_shared/runtime/export.h rename backends/{gpu_shared => vulkan_shared}/BUCK (100%) rename backends/{gpu_shared => vulkan_shared}/CMakeLists.txt (63%) rename backends/{gpu_shared => vulkan_shared}/README.md (78%) rename backends/{gpu_shared/runtime/SharedGpuContext.cpp => vulkan_shared/runtime/SharedVulkanContext.cpp} (65%) rename backends/{gpu_shared/runtime/SharedGpuContext.h => vulkan_shared/runtime/SharedVulkanContext.h} (65%) rename backends/{gpu_shared/runtime/SharedGpuContextRegistry.cpp => vulkan_shared/runtime/SharedVulkanContextRegistry.cpp} (76%) create mode 100644 backends/vulkan_shared/runtime/SharedVulkanContextRegistry.h rename backends/{gpu_shared/runtime/SharedGpuRuntimeConfig.cpp => vulkan_shared/runtime/SharedVulkanRuntimeConfig.cpp} (76%) rename backends/{gpu_shared/runtime/SharedGpuRuntimeConfig.h => vulkan_shared/runtime/SharedVulkanRuntimeConfig.h} (68%) create mode 100644 backends/vulkan_shared/runtime/export.h rename backends/{gpu_shared => vulkan_shared}/runtime/test/CMakeLists.txt (74%) rename backends/{gpu_shared/runtime/test/SharedGpuContextRegistryTest.cpp => vulkan_shared/runtime/test/SharedVulkanContextRegistryTest.cpp} (67%) rename backends/{gpu_shared/runtime/test/SharedGpuRuntimeConfigTest.cpp => vulkan_shared/runtime/test/SharedVulkanRuntimeConfigTest.cpp} (64%) rename backends/{gpu_shared => vulkan_shared}/targets.bzl (67%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ef47e54f31..8ae05f3afe3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -841,7 +841,7 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/executorch-backend-dependencies.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ExecuTorch ) if(EXECUTORCH_BUILD_VULKAN OR EXECUTORCH_BUILD_VGF) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/backends/gpu_shared) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/backends/vulkan_shared) endif() if(EXECUTORCH_BUILD_ARM_BAREMETAL diff --git a/backends/gpu_shared/runtime/SharedGpuContextRegistry.h b/backends/gpu_shared/runtime/SharedGpuContextRegistry.h deleted file mode 100644 index 1fbbed78646..00000000000 --- a/backends/gpu_shared/runtime/SharedGpuContextRegistry.h +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 - -#include -#include -#include -#include -#include - -namespace executorch { -namespace backends { -namespace gpu_shared { - -// Process-local registry used by independently initialized GPU delegates. The -// canonical target is a shared library so all delegate DSOs observe the same -// registry instance. -class EXECUTORCH_GPU_SHARED_API SharedGpuContextRegistry final { - public: - using CreateFn = std::function()>; - - static SharedGpuContextRegistry& Get(); - - SharedGpuContextRegistry(const SharedGpuContextRegistry&) = delete; - SharedGpuContextRegistry& operator=(const SharedGpuContextRegistry&) = delete; - - SharedGpuContextPtr lookup(const SharedGpuContextKey& key); - - runtime::Result lookup_or_create( - const SharedGpuContextKey& key, - CreateFn create_fn); - - runtime::Error register_context(SharedGpuContextPtr context); - - runtime::Result register_external_context( - SharedGpuContextCreateInfo create_info); - - runtime::Error unregister_context(const SharedGpuContextKey& key); - - // Test-only. The caller must ensure that there are no concurrent - // registry operations or in-flight lookup_or_create() calls. - void clear_for_testing(); - - private: - struct Entry final { - SharedGpuContextPtr context; - bool creating = false; - std::condition_variable creation_complete; - }; - - struct KeyHash final { - size_t operator()(const SharedGpuContextKey& key) const; - }; - - SharedGpuContextRegistry() = default; - - // Never release a SharedGpuContext/lifetime_anchor while this mutex is held: - // backend teardown may re-enter the registry. - std::mutex mutex_; - std::unordered_map, KeyHash> - registry_; -}; - -} // namespace gpu_shared -} // namespace backends -} // namespace executorch diff --git a/backends/gpu_shared/runtime/export.h b/backends/gpu_shared/runtime/export.h deleted file mode 100644 index 4b8385766b5..00000000000 --- a/backends/gpu_shared/runtime/export.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 - -// The shared GPU registry must have one definition across all delegate shared -// objects in a process. Export its public API from -// executorch_gpu_shared_runtime. -#if defined(_WIN32) -#if defined(EXECUTORCH_GPU_SHARED_BUILDING) -#define EXECUTORCH_GPU_SHARED_API __declspec(dllexport) -#else -#define EXECUTORCH_GPU_SHARED_API __declspec(dllimport) -#endif -#else -#define EXECUTORCH_GPU_SHARED_API __attribute__((visibility("default"))) -#endif diff --git a/backends/gpu_shared/BUCK b/backends/vulkan_shared/BUCK similarity index 100% rename from backends/gpu_shared/BUCK rename to backends/vulkan_shared/BUCK diff --git a/backends/gpu_shared/CMakeLists.txt b/backends/vulkan_shared/CMakeLists.txt similarity index 63% rename from backends/gpu_shared/CMakeLists.txt rename to backends/vulkan_shared/CMakeLists.txt index f1aa4bcdb97..c384bd625e8 100644 --- a/backends/gpu_shared/CMakeLists.txt +++ b/backends/vulkan_shared/CMakeLists.txt @@ -12,15 +12,16 @@ endif() include(GNUInstallDirs) find_package(Threads REQUIRED) -set(GPU_SHARED_VULKAN_HEADERS_PATH +set(VULKAN_SHARED_HEADERS_PATH ${EXECUTORCH_ROOT}/backends/vulkan/third-party/Vulkan-Headers ) -if(NOT EXISTS "${GPU_SHARED_VULKAN_HEADERS_PATH}/include/vulkan/vulkan.h") +if(NOT EXISTS "${VULKAN_SHARED_HEADERS_PATH}/include/vulkan/vulkan.h") message( FATAL_ERROR - "The shared GPU runtime requires the vendored Vulkan-Headers submodule. " - "Run from the repository root:\n" " git submodule update --init " + "The shared Vulkan runtime requires the vendored Vulkan-Headers submodule. " + "Run from the repository root:\n" + " git submodule update --init " "backends/vulkan/third-party/Vulkan-Headers" ) endif() @@ -29,33 +30,35 @@ endif() # observe one process-wide registry. A static copy in each DSO would create # independent registries and defeat context sharing. add_library( - executorch_gpu_shared_runtime SHARED - ${CMAKE_CURRENT_SOURCE_DIR}/runtime/SharedGpuContext.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/runtime/SharedGpuContextRegistry.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/runtime/SharedGpuRuntimeConfig.cpp + executorch_vulkan_shared_runtime SHARED + ${CMAKE_CURRENT_SOURCE_DIR}/runtime/SharedVulkanContext.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/runtime/SharedVulkanContextRegistry.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/runtime/SharedVulkanRuntimeConfig.cpp +) +add_library( + executorch::vulkan_shared_runtime ALIAS executorch_vulkan_shared_runtime ) -add_library(executorch::gpu_shared_runtime ALIAS executorch_gpu_shared_runtime) target_compile_definitions( - executorch_gpu_shared_runtime PRIVATE EXECUTORCH_GPU_SHARED_BUILDING + executorch_vulkan_shared_runtime PRIVATE EXECUTORCH_VULKAN_SHARED_BUILDING ) target_include_directories( - executorch_gpu_shared_runtime + executorch_vulkan_shared_runtime PUBLIC $ $ - $ + $ $ ) target_link_libraries( - executorch_gpu_shared_runtime + executorch_vulkan_shared_runtime PUBLIC executorch_core PRIVATE Threads::Threads ) set_target_properties( - executorch_gpu_shared_runtime + executorch_vulkan_shared_runtime PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED YES CXX_VISIBILITY_PRESET hidden @@ -63,7 +66,7 @@ set_target_properties( ) install( - TARGETS executorch_gpu_shared_runtime + TARGETS executorch_vulkan_shared_runtime EXPORT ExecuTorchTargets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} @@ -74,17 +77,18 @@ install( install( DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/runtime/ - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/executorch/backends/gpu_shared/runtime + DESTINATION + ${CMAKE_INSTALL_INCLUDEDIR}/executorch/backends/vulkan_shared/runtime FILES_MATCHING PATTERN "*.h" PATTERN "test" EXCLUDE ) -# SharedGpuContext.h is a public installed header and includes +# SharedVulkanContext.h is a public installed header and includes # . Install the same vendored headers used by the build so an # installed ExecuTorch package does not depend on an unrelated system Vulkan # SDK. -install(DIRECTORY ${GPU_SHARED_VULKAN_HEADERS_PATH}/include/ +install(DIRECTORY ${VULKAN_SHARED_HEADERS_PATH}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) diff --git a/backends/gpu_shared/README.md b/backends/vulkan_shared/README.md similarity index 78% rename from backends/gpu_shared/README.md rename to backends/vulkan_shared/README.md index 492cde153a0..6f153d3f151 100644 --- a/backends/gpu_shared/README.md +++ b/backends/vulkan_shared/README.md @@ -1,4 +1,4 @@ -# Shared GPU runtime +# Shared Vulkan runtime This component is the backend-neutral runtime bridge used by the VGF and ExecuTorch Vulkan delegates. It deliberately does not introduce a unified @@ -24,26 +24,26 @@ Context selection is configured at model load time through `RuntimeSpec` / | Key | Type | Default | Accepted values | | --- | --- | --- | --- | -| `gpu_shared_context_token` | string | `default` | Any non-empty token | -| `gpu_shared_context_mode` | string | `lookup_or_create` | `disabled`, `lookup_only`, `lookup_or_create`, `create_only` | -| `gpu_shared_group_id` | int | `0` | Any `int` value | +| `vulkan_shared_context_name` | string | `default` | Any non-empty context_name | +| `vulkan_shared_context_mode` | string | `lookup_or_create` | `disabled`, `lookup_only`, `lookup_or_create`, `create_only` | +| `vulkan_shared_group_id` | int | `0` | Any `int` value | Both delegates must receive the same option values to resolve the same registry key. ## Ownership and validation -`SharedGpuContext` carries Vulkan handles but never calls Vulkan entry points +`SharedVulkanContext` carries Vulkan handles but never calls Vulkan entry points itself. Every registered context must provide a non-null `lifetime_anchor` whose lifetime guarantees that the Vulkan instance, physical device, device, and queue -remain valid until the final `SharedGpuContextPtr` is released. For a +remain valid until the final `SharedVulkanContextPtr` is released. For a backend-created context, the anchor can own the backend runtime and perform teardown through that backend's Vulkan dispatch mechanism. For externally created Vulkan objects, the application must provide an anchor whose ownership keeps those objects alive for the same period. `unregister_context()` removes the context from the registry and prevents new -lookups; it does not revoke `SharedGpuContextPtr` instances already held by +lookups; it does not revoke `SharedVulkanContextPtr` instances already held by delegates. Actual Vulkan teardown is therefore safe only after the final outstanding context reference releases its `lifetime_anchor`. The registry itself is intentionally process-lifetime; call `unregister_context()` to remove @@ -51,7 +51,7 @@ registry discoverability before deterministic teardown. The `VkQueue` is shared process state and Vulkan queue operations require external host synchronization. Consumers must issue queue operations through -`SharedGpuContext::with_locked_queue()` so independently initialized delegates +`SharedVulkanContext::with_locked_queue()` so independently initialized delegates serialize access using the mutex stored in the shared context rather than backend-local locks. diff --git a/backends/gpu_shared/runtime/SharedGpuContext.cpp b/backends/vulkan_shared/runtime/SharedVulkanContext.cpp similarity index 65% rename from backends/gpu_shared/runtime/SharedGpuContext.cpp rename to backends/vulkan_shared/runtime/SharedVulkanContext.cpp index 81b8de51cb5..e854128ec0b 100644 --- a/backends/gpu_shared/runtime/SharedGpuContext.cpp +++ b/backends/vulkan_shared/runtime/SharedVulkanContext.cpp @@ -5,41 +5,42 @@ * LICENSE file in the root directory of this source tree. */ -#include +#include #include #include namespace executorch { namespace backends { -namespace gpu_shared { +namespace vulkan_shared { -SharedGpuContext::SharedGpuContext(SharedGpuContextCreateInfo create_info) +SharedVulkanContext::SharedVulkanContext( + SharedVulkanContextCreateInfo create_info) : create_info_(std::move(create_info)) {} -SharedGpuContext::~SharedGpuContext() = default; +SharedVulkanContext::~SharedVulkanContext() = default; -const SharedGpuContextKey& SharedGpuContext::key() const { +const SharedVulkanContextKey& SharedVulkanContext::key() const { return create_info_.key; } -VkInstance SharedGpuContext::instance() const { +VkInstance SharedVulkanContext::instance() const { return create_info_.instance; } -VkPhysicalDevice SharedGpuContext::physical_device() const { +VkPhysicalDevice SharedVulkanContext::physical_device() const { return create_info_.physical_device; } -VkDevice SharedGpuContext::device() const { +VkDevice SharedVulkanContext::device() const { return create_info_.device; } -uint32_t SharedGpuContext::queue_family_index() const { +uint32_t SharedVulkanContext::queue_family_index() const { return create_info_.queue_family_index; } -bool SharedGpuContext::has_device_extension( +bool SharedVulkanContext::has_device_extension( std::string_view extension_name) const { return std::any_of( create_info_.enabled_device_extensions.begin(), @@ -49,7 +50,7 @@ bool SharedGpuContext::has_device_extension( }); } -bool SharedGpuContext::is_valid() const { +bool SharedVulkanContext::is_valid() const { return create_info_.key.valid() && create_info_.instance != VK_NULL_HANDLE && create_info_.physical_device != VK_NULL_HANDLE && create_info_.device != VK_NULL_HANDLE && @@ -58,6 +59,6 @@ bool SharedGpuContext::is_valid() const { create_info_.lifetime_anchor != nullptr; } -} // namespace gpu_shared +} // namespace vulkan_shared } // namespace backends } // namespace executorch diff --git a/backends/gpu_shared/runtime/SharedGpuContext.h b/backends/vulkan_shared/runtime/SharedVulkanContext.h similarity index 65% rename from backends/gpu_shared/runtime/SharedGpuContext.h rename to backends/vulkan_shared/runtime/SharedVulkanContext.h index 304a1a6c801..b4bba8d3913 100644 --- a/backends/gpu_shared/runtime/SharedGpuContext.h +++ b/backends/vulkan_shared/runtime/SharedVulkanContext.h @@ -7,7 +7,7 @@ #pragma once -#include +#include #include @@ -22,25 +22,25 @@ namespace executorch { namespace backends { -namespace gpu_shared { +namespace vulkan_shared { -struct SharedGpuContextKey final { - std::string token; +struct SharedVulkanContextKey final { + std::string context_name; int group_id = 0; bool valid() const { - return !token.empty(); + return !context_name.empty(); } friend bool operator==( - const SharedGpuContextKey& lhs, - const SharedGpuContextKey& rhs) { - return lhs.group_id == rhs.group_id && lhs.token == rhs.token; + const SharedVulkanContextKey& lhs, + const SharedVulkanContextKey& rhs) { + return lhs.group_id == rhs.group_id && lhs.context_name == rhs.context_name; } friend bool operator!=( - const SharedGpuContextKey& lhs, - const SharedGpuContextKey& rhs) { + const SharedVulkanContextKey& lhs, + const SharedVulkanContextKey& rhs) { return !(lhs == rhs); } }; @@ -48,10 +48,10 @@ struct SharedGpuContextKey final { // The shared layer carries Vulkan handles but deliberately does not call Vulkan // entry points itself. Every registered context must provide a lifetime_anchor // whose lifetime guarantees that instance, physical_device, device, and queue -// remain valid until the final SharedGpuContext reference is released. The +// remain valid until the final SharedVulkanContext reference is released. The // anchor destructor may perform backend/application Vulkan teardown. -struct SharedGpuContextCreateInfo final { - SharedGpuContextKey key; +struct SharedVulkanContextCreateInfo final { + SharedVulkanContextKey key; VkInstance instance = VK_NULL_HANDLE; VkPhysicalDevice physical_device = VK_NULL_HANDLE; VkDevice device = VK_NULL_HANDLE; @@ -61,15 +61,15 @@ struct SharedGpuContextCreateInfo final { std::shared_ptr lifetime_anchor; }; -class EXECUTORCH_GPU_SHARED_API SharedGpuContext final { +class EXECUTORCH_VULKAN_SHARED_API SharedVulkanContext final { public: - explicit SharedGpuContext(SharedGpuContextCreateInfo create_info); - ~SharedGpuContext(); + explicit SharedVulkanContext(SharedVulkanContextCreateInfo create_info); + ~SharedVulkanContext(); - SharedGpuContext(const SharedGpuContext&) = delete; - SharedGpuContext& operator=(const SharedGpuContext&) = delete; + SharedVulkanContext(const SharedVulkanContext&) = delete; + SharedVulkanContext& operator=(const SharedVulkanContext&) = delete; - const SharedGpuContextKey& key() const; + const SharedVulkanContextKey& key() const; VkInstance instance() const; VkPhysicalDevice physical_device() const; VkDevice device() const; @@ -89,12 +89,12 @@ class EXECUTORCH_GPU_SHARED_API SharedGpuContext final { bool is_valid() const; private: - SharedGpuContextCreateInfo create_info_; + SharedVulkanContextCreateInfo create_info_; mutable std::mutex queue_mutex_; }; -using SharedGpuContextPtr = std::shared_ptr; +using SharedVulkanContextPtr = std::shared_ptr; -} // namespace gpu_shared +} // namespace vulkan_shared } // namespace backends } // namespace executorch diff --git a/backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp b/backends/vulkan_shared/runtime/SharedVulkanContextRegistry.cpp similarity index 76% rename from backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp rename to backends/vulkan_shared/runtime/SharedVulkanContextRegistry.cpp index a8d3037169a..d3771cfc82d 100644 --- a/backends/gpu_shared/runtime/SharedGpuContextRegistry.cpp +++ b/backends/vulkan_shared/runtime/SharedVulkanContextRegistry.cpp @@ -5,38 +5,38 @@ * LICENSE file in the root directory of this source tree. */ -#include +#include #include namespace executorch { namespace backends { -namespace gpu_shared { +namespace vulkan_shared { -SharedGpuContextRegistry& SharedGpuContextRegistry::Get() { +SharedVulkanContextRegistry& SharedVulkanContextRegistry::Get() { // The default context is process-persistent. Intentionally do not register a // static destructor: delegate DSOs may be unloaded before their lifetime // anchors, so teardown must be explicit through unregister_context(). - static auto* registry = new SharedGpuContextRegistry(); + static auto* registry = new SharedVulkanContextRegistry(); return *registry; } -size_t SharedGpuContextRegistry::KeyHash::operator()( - const SharedGpuContextKey& key) const { - const size_t token_hash = std::hash{}(key.token); +size_t SharedVulkanContextRegistry::KeyHash::operator()( + const SharedVulkanContextKey& key) const { + const size_t token_hash = std::hash{}(key.context_name); const size_t group_hash = std::hash{}(key.group_id); return token_hash ^ (group_hash + static_cast(0x9e3779b9) + (token_hash << 6) + (token_hash >> 2)); } -SharedGpuContextPtr SharedGpuContextRegistry::lookup( - const SharedGpuContextKey& key) { +SharedVulkanContextPtr SharedVulkanContextRegistry::lookup( + const SharedVulkanContextKey& key) { if (!key.valid()) { return nullptr; } - SharedGpuContextPtr stale_context; + SharedVulkanContextPtr stale_context; { std::lock_guard lock(mutex_); auto it = registry_.find(key); @@ -62,15 +62,16 @@ SharedGpuContextPtr SharedGpuContextRegistry::lookup( return nullptr; } -runtime::Result SharedGpuContextRegistry::lookup_or_create( - const SharedGpuContextKey& key, +runtime::Result +SharedVulkanContextRegistry::lookup_or_create( + const SharedVulkanContextKey& key, CreateFn create_fn) { if (!key.valid() || !create_fn) { return runtime::Error::InvalidArgument; } std::shared_ptr entry; - SharedGpuContextPtr stale_context; + SharedVulkanContextPtr stale_context; { std::unique_lock lock(mutex_); @@ -95,7 +96,7 @@ runtime::Result SharedGpuContextRegistry::lookup_or_create( auto maybe_created = create_fn(); runtime::Error create_error = runtime::Error::Ok; - SharedGpuContextPtr created; + SharedVulkanContextPtr created; if (!maybe_created.ok()) { create_error = maybe_created.error(); } else { @@ -106,7 +107,7 @@ runtime::Result SharedGpuContextRegistry::lookup_or_create( } } - SharedGpuContextPtr selected; + SharedVulkanContextPtr selected; { std::lock_guard lock(mutex_); @@ -130,8 +131,8 @@ runtime::Result SharedGpuContextRegistry::lookup_or_create( : create_error; } -runtime::Error SharedGpuContextRegistry::register_context( - SharedGpuContextPtr context) { +runtime::Error SharedVulkanContextRegistry::register_context( + SharedVulkanContextPtr context) { if (!context || !context->is_valid()) { return runtime::Error::InvalidArgument; } @@ -153,10 +154,10 @@ runtime::Error SharedGpuContextRegistry::register_context( return runtime::Error::Ok; } -runtime::Result -SharedGpuContextRegistry::register_external_context( - SharedGpuContextCreateInfo create_info) { - auto context = std::make_shared(std::move(create_info)); +runtime::Result +SharedVulkanContextRegistry::register_external_context( + SharedVulkanContextCreateInfo create_info) { + auto context = std::make_shared(std::move(create_info)); const runtime::Error error = register_context(context); if (error != runtime::Error::Ok) { return error; @@ -164,8 +165,8 @@ SharedGpuContextRegistry::register_external_context( return context; } -runtime::Error SharedGpuContextRegistry::unregister_context( - const SharedGpuContextKey& key) { +runtime::Error SharedVulkanContextRegistry::unregister_context( + const SharedVulkanContextKey& key) { if (!key.valid()) { return runtime::Error::InvalidArgument; } @@ -182,8 +183,8 @@ runtime::Error SharedGpuContextRegistry::unregister_context( } // Unlink the entry under the registry lock, but retain ownership locally so - // SharedGpuContext/lifetime_anchor destruction cannot run while mutex_ is - // held. Backend teardown is allowed to re-enter this registry. + // SharedVulkanContext/lifetime_anchor destruction cannot run while mutex_ + // is held. Backend teardown is allowed to re-enter this registry. removed_entry = std::move(it->second); registry_.erase(it); } @@ -192,7 +193,7 @@ runtime::Error SharedGpuContextRegistry::unregister_context( return runtime::Error::Ok; } -void SharedGpuContextRegistry::clear_for_testing() { +void SharedVulkanContextRegistry::clear_for_testing() { decltype(registry_) removed_entries; { std::lock_guard lock(mutex_); @@ -209,6 +210,6 @@ void SharedGpuContextRegistry::clear_for_testing() { removed_entries.clear(); } -} // namespace gpu_shared +} // namespace vulkan_shared } // namespace backends } // namespace executorch diff --git a/backends/vulkan_shared/runtime/SharedVulkanContextRegistry.h b/backends/vulkan_shared/runtime/SharedVulkanContextRegistry.h new file mode 100644 index 00000000000..b41cb660ce2 --- /dev/null +++ b/backends/vulkan_shared/runtime/SharedVulkanContextRegistry.h @@ -0,0 +1,77 @@ +/* + * 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 + +#include +#include +#include +#include +#include + +namespace executorch { +namespace backends { +namespace vulkan_shared { + +// Process-local registry used by independently initialized GPU delegates. The +// canonical target is a shared library so all delegate DSOs observe the same +// registry instance. +class EXECUTORCH_VULKAN_SHARED_API SharedVulkanContextRegistry final { + public: + using CreateFn = std::function()>; + + static SharedVulkanContextRegistry& Get(); + + SharedVulkanContextRegistry(const SharedVulkanContextRegistry&) = delete; + SharedVulkanContextRegistry& operator=(const SharedVulkanContextRegistry&) = + delete; + + SharedVulkanContextPtr lookup(const SharedVulkanContextKey& key); + + runtime::Result lookup_or_create( + const SharedVulkanContextKey& key, + CreateFn create_fn); + + runtime::Error register_context(SharedVulkanContextPtr context); + + runtime::Result register_external_context( + SharedVulkanContextCreateInfo create_info); + + runtime::Error unregister_context(const SharedVulkanContextKey& key); + + // Test-only. The caller must ensure that there are no concurrent + // registry operations or in-flight lookup_or_create() calls. + void clear_for_testing(); + + private: + struct Entry final { + SharedVulkanContextPtr context; + bool creating = false; + std::condition_variable creation_complete; + }; + + struct KeyHash final { + size_t operator()(const SharedVulkanContextKey& key) const; + }; + + SharedVulkanContextRegistry() = default; + + // Never release a SharedVulkanContext/lifetime_anchor while this mutex is + // held: backend teardown may re-enter the registry. + std::mutex mutex_; + std::unordered_map, KeyHash> + registry_; +}; + +} // namespace vulkan_shared +} // namespace backends +} // namespace executorch diff --git a/backends/gpu_shared/runtime/SharedGpuRuntimeConfig.cpp b/backends/vulkan_shared/runtime/SharedVulkanRuntimeConfig.cpp similarity index 76% rename from backends/gpu_shared/runtime/SharedGpuRuntimeConfig.cpp rename to backends/vulkan_shared/runtime/SharedVulkanRuntimeConfig.cpp index 0a6603086c6..4fc75641fff 100644 --- a/backends/gpu_shared/runtime/SharedGpuRuntimeConfig.cpp +++ b/backends/vulkan_shared/runtime/SharedVulkanRuntimeConfig.cpp @@ -5,13 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -#include +#include #include namespace executorch { namespace backends { -namespace gpu_shared { +namespace vulkan_shared { namespace { runtime::Result parse_context_mode(const char* value) { @@ -38,15 +38,16 @@ runtime::Result parse_context_mode(const char* value) { // This API is consumed by the Vulkan/VGF delegate integration follow-up PRs. // The phase-2 runtime library intentionally has no production caller yet. // cppcheck-suppress unusedFunction -runtime::Result parse_shared_gpu_runtime_config( +runtime::Result parse_shared_vulkan_runtime_config( const runtime::BackendInitContext& context) { - SharedGpuRuntimeConfig config; + SharedVulkanRuntimeConfig config; - auto token = context.get_runtime_spec(kSharedContextTokenOption); - if (token.ok()) { - config.token = token.get(); - } else if (token.error() != runtime::Error::NotFound) { - return token.error(); + auto context_name = + context.get_runtime_spec(kSharedContextNameOption); + if (context_name.ok()) { + config.context_name = context_name.get(); + } else if (context_name.error() != runtime::Error::NotFound) { + return context_name.error(); } auto mode = context.get_runtime_spec(kSharedContextModeOption); @@ -67,13 +68,13 @@ runtime::Result parse_shared_gpu_runtime_config( return group_id.error(); } - if (config.enabled() && config.token.empty()) { + if (config.enabled() && config.context_name.empty()) { return runtime::Error::InvalidArgument; } return config; } -} // namespace gpu_shared +} // namespace vulkan_shared } // namespace backends } // namespace executorch diff --git a/backends/gpu_shared/runtime/SharedGpuRuntimeConfig.h b/backends/vulkan_shared/runtime/SharedVulkanRuntimeConfig.h similarity index 68% rename from backends/gpu_shared/runtime/SharedGpuRuntimeConfig.h rename to backends/vulkan_shared/runtime/SharedVulkanRuntimeConfig.h index b80dbd32c23..bae9f051c24 100644 --- a/backends/gpu_shared/runtime/SharedGpuRuntimeConfig.h +++ b/backends/vulkan_shared/runtime/SharedVulkanRuntimeConfig.h @@ -7,7 +7,7 @@ #pragma once -#include +#include #include #include @@ -16,11 +16,11 @@ namespace executorch { namespace backends { -namespace gpu_shared { +namespace vulkan_shared { -inline constexpr char kSharedContextTokenOption[] = "gpu_shared_context_token"; -inline constexpr char kSharedContextModeOption[] = "gpu_shared_context_mode"; -inline constexpr char kSharedGroupIdOption[] = "gpu_shared_group_id"; +inline constexpr char kSharedContextNameOption[] = "vulkan_shared_context_name"; +inline constexpr char kSharedContextModeOption[] = "vulkan_shared_context_mode"; +inline constexpr char kSharedGroupIdOption[] = "vulkan_shared_group_id"; enum class SharedContextMode : uint8_t { kDisabled = 0, @@ -32,8 +32,8 @@ enum class SharedContextMode : uint8_t { // Load-time configuration shared by the VGF and Vulkan delegates. These values // are RuntimeSpec options: context selection is a deployment concern and must // not be serialized into a backend CompileSpec or a .pte file. -struct SharedGpuRuntimeConfig final { - std::string token = "default"; +struct SharedVulkanRuntimeConfig final { + std::string context_name = "default"; int group_id = 0; SharedContextMode context_mode = SharedContextMode::kLookupOrCreate; @@ -54,9 +54,9 @@ struct SharedGpuRuntimeConfig final { } }; -EXECUTORCH_GPU_SHARED_API runtime::Result -parse_shared_gpu_runtime_config(const runtime::BackendInitContext& context); +EXECUTORCH_VULKAN_SHARED_API runtime::Result +parse_shared_vulkan_runtime_config(const runtime::BackendInitContext& context); -} // namespace gpu_shared +} // namespace vulkan_shared } // namespace backends } // namespace executorch diff --git a/backends/vulkan_shared/runtime/export.h b/backends/vulkan_shared/runtime/export.h new file mode 100644 index 00000000000..f780d5ba1c0 --- /dev/null +++ b/backends/vulkan_shared/runtime/export.h @@ -0,0 +1,21 @@ +/* + * 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 + +// The shared Vulkan registry must have one definition across all delegate +// shared objects in a process. Export its public API from +// executorch_vulkan_shared_runtime. +#if defined(_WIN32) +#if defined(EXECUTORCH_VULKAN_SHARED_BUILDING) +#define EXECUTORCH_VULKAN_SHARED_API __declspec(dllexport) +#else +#define EXECUTORCH_VULKAN_SHARED_API __declspec(dllimport) +#endif +#else +#define EXECUTORCH_VULKAN_SHARED_API __attribute__((visibility("default"))) +#endif diff --git a/backends/gpu_shared/runtime/test/CMakeLists.txt b/backends/vulkan_shared/runtime/test/CMakeLists.txt similarity index 74% rename from backends/gpu_shared/runtime/test/CMakeLists.txt rename to backends/vulkan_shared/runtime/test/CMakeLists.txt index ee0c78eb0c0..c8e1869948f 100644 --- a/backends/gpu_shared/runtime/test/CMakeLists.txt +++ b/backends/vulkan_shared/runtime/test/CMakeLists.txt @@ -5,12 +5,12 @@ add_executable( shared_gpu_runtime_test - ${CMAKE_CURRENT_SOURCE_DIR}/SharedGpuContextRegistryTest.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/SharedGpuRuntimeConfigTest.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/SharedVulkanContextRegistryTest.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/SharedVulkanRuntimeConfigTest.cpp ) target_link_libraries( - shared_gpu_runtime_test PRIVATE executorch_gpu_shared_runtime + shared_gpu_runtime_test PRIVATE executorch_vulkan_shared_runtime Threads::Threads ) diff --git a/backends/gpu_shared/runtime/test/SharedGpuContextRegistryTest.cpp b/backends/vulkan_shared/runtime/test/SharedVulkanContextRegistryTest.cpp similarity index 67% rename from backends/gpu_shared/runtime/test/SharedGpuContextRegistryTest.cpp rename to backends/vulkan_shared/runtime/test/SharedVulkanContextRegistryTest.cpp index 480ba03d468..59a1b61c478 100644 --- a/backends/gpu_shared/runtime/test/SharedGpuContextRegistryTest.cpp +++ b/backends/vulkan_shared/runtime/test/SharedVulkanContextRegistryTest.cpp @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#include +#include #include // Cppcheck's lint environment may not expand the gtest macros. @@ -26,11 +26,11 @@ #include #include -using executorch::backends::gpu_shared::SharedGpuContext; -using executorch::backends::gpu_shared::SharedGpuContextCreateInfo; -using executorch::backends::gpu_shared::SharedGpuContextKey; -using executorch::backends::gpu_shared::SharedGpuContextPtr; -using executorch::backends::gpu_shared::SharedGpuContextRegistry; +using executorch::backends::vulkan_shared::SharedVulkanContext; +using executorch::backends::vulkan_shared::SharedVulkanContextCreateInfo; +using executorch::backends::vulkan_shared::SharedVulkanContextKey; +using executorch::backends::vulkan_shared::SharedVulkanContextPtr; +using executorch::backends::vulkan_shared::SharedVulkanContextRegistry; using executorch::runtime::Error; using executorch::runtime::Result; @@ -41,10 +41,10 @@ Handle fake_handle(uintptr_t value) { return reinterpret_cast(value); } -SharedGpuContextCreateInfo make_create_info( - SharedGpuContextKey key, +SharedVulkanContextCreateInfo make_create_info( + SharedVulkanContextKey key, uintptr_t handle_base = 1) { - SharedGpuContextCreateInfo info; + SharedVulkanContextCreateInfo info; info.key = std::move(key); info.instance = fake_handle(handle_base); info.physical_device = fake_handle(handle_base + 1); @@ -66,7 +66,7 @@ struct RegistryLookupProbeState final { class RegistryLookupLifetimeAnchor final { public: RegistryLookupLifetimeAnchor( - SharedGpuContextKey lookup_key, + SharedVulkanContextKey lookup_key, std::shared_ptr state, std::atomic* lookup_completed_during_destruction) : lookup_key_(std::move(lookup_key)), @@ -80,7 +80,7 @@ class RegistryLookupLifetimeAnchor final { // returns and unregister_context() releases the mutex. The timeout prevents // the regression test itself from deadlocking on the buggy implementation. std::thread([lookup_key = lookup_key_, state = state_]() { - (void)SharedGpuContextRegistry::Get().lookup(lookup_key); + (void)SharedVulkanContextRegistry::Get().lookup(lookup_key); { std::lock_guard lock(state->mutex); state->complete = true; @@ -97,25 +97,25 @@ class RegistryLookupLifetimeAnchor final { } private: - SharedGpuContextKey lookup_key_; + SharedVulkanContextKey lookup_key_; std::shared_ptr state_; std::atomic* lookup_completed_during_destruction_; }; -class SharedGpuContextRegistryTest : public ::testing::Test { +class SharedVulkanContextRegistryTest : public ::testing::Test { protected: void SetUp() override { - SharedGpuContextRegistry::Get().clear_for_testing(); + SharedVulkanContextRegistry::Get().clear_for_testing(); } void TearDown() override { - SharedGpuContextRegistry::Get().clear_for_testing(); + SharedVulkanContextRegistry::Get().clear_for_testing(); } }; // cppcheck-suppress unusedFunction -TEST_F(SharedGpuContextRegistryTest, RegistryOwnsPersistentContext) { - const SharedGpuContextKey key{"scene0", 3}; +TEST_F(SharedVulkanContextRegistryTest, RegistryOwnsPersistentContext) { + const SharedVulkanContextKey key{"scene0", 3}; std::weak_ptr owner_weak; { @@ -124,23 +124,25 @@ TEST_F(SharedGpuContextRegistryTest, RegistryOwnsPersistentContext) { auto info = make_create_info(key); info.lifetime_anchor = owner; - auto registered = SharedGpuContextRegistry::Get().register_external_context( - std::move(info)); + auto registered = + SharedVulkanContextRegistry::Get().register_external_context( + std::move(info)); ASSERT_TRUE(registered.ok()); } EXPECT_FALSE(owner_weak.expired()); - EXPECT_NE(SharedGpuContextRegistry::Get().lookup(key), nullptr); - EXPECT_EQ(SharedGpuContextRegistry::Get().unregister_context(key), Error::Ok); + EXPECT_NE(SharedVulkanContextRegistry::Get().lookup(key), nullptr); + EXPECT_EQ( + SharedVulkanContextRegistry::Get().unregister_context(key), Error::Ok); EXPECT_TRUE(owner_weak.expired()); } // cppcheck-suppress unusedFunction TEST_F( - SharedGpuContextRegistryTest, + SharedVulkanContextRegistryTest, UnregisterDestroysLifetimeAnchorOutsideRegistryLock) { - const SharedGpuContextKey key{"scene0", 9}; - const SharedGpuContextKey probe_key{"probe", 9}; + const SharedVulkanContextKey key{"scene0", 9}; + const SharedVulkanContextKey probe_key{"probe", 9}; std::atomic lookup_completed_during_destruction{false}; auto probe_state = std::make_shared(); @@ -149,15 +151,17 @@ TEST_F( info.lifetime_anchor = std::make_shared( probe_key, probe_state, &lookup_completed_during_destruction); - auto registered = SharedGpuContextRegistry::Get().register_external_context( - std::move(info)); + auto registered = + SharedVulkanContextRegistry::Get().register_external_context( + std::move(info)); ASSERT_TRUE(registered.ok()); } - // The registry is now the only owner of the SharedGpuContext. Unregistering - // therefore destroys its lifetime anchor. The probe must be able to acquire - // the registry mutex before that destruction returns. - EXPECT_EQ(SharedGpuContextRegistry::Get().unregister_context(key), Error::Ok); + // The registry is now the only owner of the SharedVulkanContext. + // Unregistering therefore destroys its lifetime anchor. The probe must be + // able to acquire the registry mutex before that destruction returns. + EXPECT_EQ( + SharedVulkanContextRegistry::Get().unregister_context(key), Error::Ok); EXPECT_TRUE(lookup_completed_during_destruction.load()); // On a broken implementation the probe only completes after unregister has @@ -171,15 +175,15 @@ TEST_F( } // cppcheck-suppress unusedFunction -TEST_F(SharedGpuContextRegistryTest, LookupOrCreateRunsCreatorOnce) { - const SharedGpuContextKey key{"scene0", 4}; +TEST_F(SharedVulkanContextRegistryTest, LookupOrCreateRunsCreatorOnce) { + const SharedVulkanContextKey key{"scene0", 4}; std::atomic create_count{0}; std::mutex creator_mutex; std::condition_variable creator_cv; bool creator_entered = false; bool allow_creator_to_finish = false; - auto create_fn = [&]() -> Result { + auto create_fn = [&]() -> Result { ++create_count; { std::unique_lock lock(creator_mutex); @@ -187,18 +191,18 @@ TEST_F(SharedGpuContextRegistryTest, LookupOrCreateRunsCreatorOnce) { creator_cv.notify_all(); creator_cv.wait(lock, [&]() { return allow_creator_to_finish; }); } - return std::make_shared(make_create_info(key)); + return std::make_shared(make_create_info(key)); }; constexpr size_t kThreadCount = 8; - std::vector results(kThreadCount); + std::vector results(kThreadCount); std::vector errors(kThreadCount, Error::Internal); std::vector threads; threads.reserve(kThreadCount); for (size_t i = 0; i < kThreadCount; ++i) { threads.emplace_back([&, i]() { auto result = - SharedGpuContextRegistry::Get().lookup_or_create(key, create_fn); + SharedVulkanContextRegistry::Get().lookup_or_create(key, create_fn); errors[i] = result.error(); if (result.ok()) { results[i] = result.get(); @@ -225,40 +229,42 @@ TEST_F(SharedGpuContextRegistryTest, LookupOrCreateRunsCreatorOnce) { } // cppcheck-suppress unusedFunction -TEST_F(SharedGpuContextRegistryTest, RejectsDifferentDuplicateContext) { - const SharedGpuContextKey key{"scene0", 5}; - auto first = std::make_shared(make_create_info(key, 10)); - auto second = std::make_shared(make_create_info(key, 20)); +TEST_F(SharedVulkanContextRegistryTest, RejectsDifferentDuplicateContext) { + const SharedVulkanContextKey key{"scene0", 5}; + auto first = std::make_shared(make_create_info(key, 10)); + auto second = + std::make_shared(make_create_info(key, 20)); - EXPECT_EQ(SharedGpuContextRegistry::Get().register_context(first), Error::Ok); EXPECT_EQ( - SharedGpuContextRegistry::Get().register_context(second), + SharedVulkanContextRegistry::Get().register_context(first), Error::Ok); + EXPECT_EQ( + SharedVulkanContextRegistry::Get().register_context(second), Error::AlreadyLoaded); - EXPECT_EQ(SharedGpuContextRegistry::Get().lookup(key), first); + EXPECT_EQ(SharedVulkanContextRegistry::Get().lookup(key), first); } // cppcheck-suppress unusedFunction -TEST_F(SharedGpuContextRegistryTest, ValidatesContextIdentity) { - const SharedGpuContextKey requested_key{"scene0", 6}; - const SharedGpuContextKey returned_key{"other", 6}; +TEST_F(SharedVulkanContextRegistryTest, ValidatesContextIdentity) { + const SharedVulkanContextKey requested_key{"scene0", 6}; + const SharedVulkanContextKey returned_key{"other", 6}; - auto result = SharedGpuContextRegistry::Get().lookup_or_create( - requested_key, [&]() -> Result { - return std::make_shared( + auto result = SharedVulkanContextRegistry::Get().lookup_or_create( + requested_key, [&]() -> Result { + return std::make_shared( make_create_info(returned_key)); }); ASSERT_FALSE(result.ok()); EXPECT_EQ(result.error(), Error::InvalidArgument); - EXPECT_EQ(SharedGpuContextRegistry::Get().lookup(requested_key), nullptr); + EXPECT_EQ(SharedVulkanContextRegistry::Get().lookup(requested_key), nullptr); } // cppcheck-suppress unusedFunction -TEST_F(SharedGpuContextRegistryTest, ReportsDeclaredDeviceExtensions) { - const SharedGpuContextKey key{"scene0", 7}; +TEST_F(SharedVulkanContextRegistryTest, ReportsDeclaredDeviceExtensions) { + const SharedVulkanContextKey key{"scene0", 7}; auto info = make_create_info(key); info.enabled_device_extensions = {"VK_ARM_tensors", "VK_ARM_data_graph"}; - SharedGpuContext context(std::move(info)); + SharedVulkanContext context(std::move(info)); EXPECT_TRUE(context.has_device_extension("VK_ARM_tensors")); EXPECT_TRUE(context.has_device_extension("VK_ARM_data_graph")); @@ -266,12 +272,12 @@ TEST_F(SharedGpuContextRegistryTest, ReportsDeclaredDeviceExtensions) { } // cppcheck-suppress unusedFunction -TEST_F(SharedGpuContextRegistryTest, RejectsContextWithoutLifetimeAnchor) { - const SharedGpuContextKey key{"scene0", 10}; +TEST_F(SharedVulkanContextRegistryTest, RejectsContextWithoutLifetimeAnchor) { + const SharedVulkanContextKey key{"scene0", 10}; auto info = make_create_info(key); info.lifetime_anchor.reset(); - auto result = SharedGpuContextRegistry::Get().register_external_context( + auto result = SharedVulkanContextRegistry::Get().register_external_context( std::move(info)); ASSERT_FALSE(result.ok()); @@ -280,11 +286,11 @@ TEST_F(SharedGpuContextRegistryTest, RejectsContextWithoutLifetimeAnchor) { // cppcheck-suppress unusedFunction TEST_F( - SharedGpuContextRegistryTest, + SharedVulkanContextRegistryTest, UnregisterKeepsLifetimeAnchorAliveWhileContextIsReferenced) { - const SharedGpuContextKey key{"scene0", 11}; + const SharedVulkanContextKey key{"scene0", 11}; std::weak_ptr owner_weak; - SharedGpuContextPtr held_context; + SharedVulkanContextPtr held_context; { auto owner = std::make_shared(17); @@ -293,17 +299,19 @@ TEST_F( auto info = make_create_info(key); info.lifetime_anchor = owner; - auto registered = SharedGpuContextRegistry::Get().register_external_context( - std::move(info)); + auto registered = + SharedVulkanContextRegistry::Get().register_external_context( + std::move(info)); ASSERT_TRUE(registered.ok()); - held_context = SharedGpuContextRegistry::Get().lookup(key); + held_context = SharedVulkanContextRegistry::Get().lookup(key); ASSERT_NE(held_context, nullptr); } EXPECT_FALSE(owner_weak.expired()); - EXPECT_EQ(SharedGpuContextRegistry::Get().unregister_context(key), Error::Ok); - EXPECT_EQ(SharedGpuContextRegistry::Get().lookup(key), nullptr); + EXPECT_EQ( + SharedVulkanContextRegistry::Get().unregister_context(key), Error::Ok); + EXPECT_EQ(SharedVulkanContextRegistry::Get().lookup(key), nullptr); // unregister_context() removes registry ownership only. An existing delegate // reference must continue to keep the underlying Vulkan objects alive. @@ -314,9 +322,9 @@ TEST_F( } // cppcheck-suppress unusedFunction -TEST_F(SharedGpuContextRegistryTest, SerializesSharedQueueAccess) { - const SharedGpuContextKey key{"scene0", 12}; - auto context = std::make_shared(make_create_info(key)); +TEST_F(SharedVulkanContextRegistryTest, SerializesSharedQueueAccess) { + const SharedVulkanContextKey key{"scene0", 12}; + auto context = std::make_shared(make_create_info(key)); std::mutex state_mutex; std::condition_variable state_cv; @@ -400,11 +408,11 @@ TEST_F(SharedGpuContextRegistryTest, SerializesSharedQueueAccess) { } // cppcheck-suppress unusedFunction -TEST_F(SharedGpuContextRegistryTest, RejectsIncompleteExternalContext) { - SharedGpuContextCreateInfo info; +TEST_F(SharedVulkanContextRegistryTest, RejectsIncompleteExternalContext) { + SharedVulkanContextCreateInfo info; info.key = {"scene0", 8}; - auto result = SharedGpuContextRegistry::Get().register_external_context( + auto result = SharedVulkanContextRegistry::Get().register_external_context( std::move(info)); ASSERT_FALSE(result.ok()); diff --git a/backends/gpu_shared/runtime/test/SharedGpuRuntimeConfigTest.cpp b/backends/vulkan_shared/runtime/test/SharedVulkanRuntimeConfigTest.cpp similarity index 64% rename from backends/gpu_shared/runtime/test/SharedGpuRuntimeConfigTest.cpp rename to backends/vulkan_shared/runtime/test/SharedVulkanRuntimeConfigTest.cpp index a8a55e6b974..372f7eda3ba 100644 --- a/backends/gpu_shared/runtime/test/SharedGpuRuntimeConfigTest.cpp +++ b/backends/vulkan_shared/runtime/test/SharedVulkanRuntimeConfigTest.cpp @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#include +#include #include #include @@ -15,11 +15,11 @@ #define TEST(test_suite_name, test_name) void test_suite_name##_##test_name() #endif -using executorch::backends::gpu_shared::kSharedContextModeOption; -using executorch::backends::gpu_shared::kSharedContextTokenOption; -using executorch::backends::gpu_shared::kSharedGroupIdOption; -using executorch::backends::gpu_shared::parse_shared_gpu_runtime_config; -using executorch::backends::gpu_shared::SharedContextMode; +using executorch::backends::vulkan_shared::kSharedContextModeOption; +using executorch::backends::vulkan_shared::kSharedContextNameOption; +using executorch::backends::vulkan_shared::kSharedGroupIdOption; +using executorch::backends::vulkan_shared::parse_shared_vulkan_runtime_config; +using executorch::backends::vulkan_shared::SharedContextMode; using executorch::runtime::BackendInitContext; using executorch::runtime::BackendOption; using executorch::runtime::BackendOptions; @@ -36,79 +36,79 @@ BackendInitContext make_context(BackendOptions& options) { } // cppcheck-suppress unusedFunction -TEST(SharedGpuRuntimeConfigTest, UsesPersistentSharedDefaults) { +TEST(SharedVulkanRuntimeConfigTest, UsesPersistentSharedDefaults) { BackendInitContext context(nullptr); - auto result = parse_shared_gpu_runtime_config(context); + auto result = parse_shared_vulkan_runtime_config(context); ASSERT_TRUE(result.ok()); - EXPECT_EQ(result->token, "default"); + EXPECT_EQ(result->context_name, "default"); EXPECT_EQ(result->group_id, 0); EXPECT_EQ(result->context_mode, SharedContextMode::kLookupOrCreate); } // cppcheck-suppress unusedFunction -TEST(SharedGpuRuntimeConfigTest, ParsesRuntimeOptions) { +TEST(SharedVulkanRuntimeConfigTest, ParsesRuntimeOptions) { BackendOptions<3> options; - ASSERT_EQ(options.set_option(kSharedContextTokenOption, "scene0"), Error::Ok); + ASSERT_EQ(options.set_option(kSharedContextNameOption, "scene0"), Error::Ok); ASSERT_EQ( options.set_option(kSharedContextModeOption, "lookup_only"), Error::Ok); ASSERT_EQ(options.set_option(kSharedGroupIdOption, 7), Error::Ok); auto context = make_context(options); - auto result = parse_shared_gpu_runtime_config(context); + auto result = parse_shared_vulkan_runtime_config(context); ASSERT_TRUE(result.ok()); - EXPECT_EQ(result->token, "scene0"); + EXPECT_EQ(result->context_name, "scene0"); EXPECT_EQ(result->group_id, 7); EXPECT_TRUE(result->lookup_only()); } // cppcheck-suppress unusedFunction -TEST(SharedGpuRuntimeConfigTest, ParsesDisabledMode) { +TEST(SharedVulkanRuntimeConfigTest, ParsesDisabledMode) { BackendOptions<1> options; ASSERT_EQ( options.set_option(kSharedContextModeOption, "disabled"), Error::Ok); auto context = make_context(options); - auto result = parse_shared_gpu_runtime_config(context); + auto result = parse_shared_vulkan_runtime_config(context); ASSERT_TRUE(result.ok()); EXPECT_FALSE(result->enabled()); } // cppcheck-suppress unusedFunction -TEST(SharedGpuRuntimeConfigTest, RejectsUnknownMode) { +TEST(SharedVulkanRuntimeConfigTest, RejectsUnknownMode) { BackendOptions<1> options; ASSERT_EQ( options.set_option(kSharedContextModeOption, "automatic"), Error::Ok); auto context = make_context(options); - auto result = parse_shared_gpu_runtime_config(context); + auto result = parse_shared_vulkan_runtime_config(context); ASSERT_FALSE(result.ok()); EXPECT_EQ(result.error(), Error::InvalidArgument); } // cppcheck-suppress unusedFunction -TEST(SharedGpuRuntimeConfigTest, RejectsWrongRuntimeOptionType) { +TEST(SharedVulkanRuntimeConfigTest, RejectsWrongRuntimeOptionType) { BackendOptions<1> options; ASSERT_EQ(options.set_option(kSharedGroupIdOption, "seven"), Error::Ok); auto context = make_context(options); - auto result = parse_shared_gpu_runtime_config(context); + auto result = parse_shared_vulkan_runtime_config(context); ASSERT_FALSE(result.ok()); EXPECT_EQ(result.error(), Error::InvalidArgument); } // cppcheck-suppress unusedFunction -TEST(SharedGpuRuntimeConfigTest, RejectsEmptyTokenWhenEnabled) { +TEST(SharedVulkanRuntimeConfigTest, RejectsEmptyTokenWhenEnabled) { BackendOptions<1> options; - ASSERT_EQ(options.set_option(kSharedContextTokenOption, ""), Error::Ok); + ASSERT_EQ(options.set_option(kSharedContextNameOption, ""), Error::Ok); auto context = make_context(options); - auto result = parse_shared_gpu_runtime_config(context); + auto result = parse_shared_vulkan_runtime_config(context); ASSERT_FALSE(result.ok()); EXPECT_EQ(result.error(), Error::InvalidArgument); diff --git a/backends/gpu_shared/targets.bzl b/backends/vulkan_shared/targets.bzl similarity index 67% rename from backends/gpu_shared/targets.bzl rename to backends/vulkan_shared/targets.bzl index 05d5769c829..c10f398ea86 100644 --- a/backends/gpu_shared/targets.bzl +++ b/backends/vulkan_shared/targets.bzl @@ -2,26 +2,26 @@ load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime") def define_common_targets(): - """Defines the shared GPU runtime and its unit tests.""" + """Defines the shared Vulkan runtime and its unit tests.""" # This target must be shareable rather than force-static. VGF and Vulkan can # live in separate DSOs, but both must resolve the same process registry. runtime.cxx_library( name = "runtime", srcs = [ - "runtime/SharedGpuContext.cpp", - "runtime/SharedGpuContextRegistry.cpp", - "runtime/SharedGpuRuntimeConfig.cpp", + "runtime/SharedVulkanContext.cpp", + "runtime/SharedVulkanContextRegistry.cpp", + "runtime/SharedVulkanRuntimeConfig.cpp", ], exported_headers = [ - "runtime/SharedGpuContext.h", - "runtime/SharedGpuContextRegistry.h", - "runtime/SharedGpuRuntimeConfig.h", + "runtime/SharedVulkanContext.h", + "runtime/SharedVulkanContextRegistry.h", + "runtime/SharedVulkanRuntimeConfig.h", "runtime/export.h", ], force_static = False, preprocessor_flags = [ - "-DEXECUTORCH_GPU_SHARED_BUILDING", + "-DEXECUTORCH_VULKAN_SHARED_BUILDING", ], visibility = ["PUBLIC"], exported_deps = [ @@ -33,7 +33,7 @@ def define_common_targets(): runtime.cxx_test( name = "shared_gpu_runtime_config_test", - srcs = ["runtime/test/SharedGpuRuntimeConfigTest.cpp"], + srcs = ["runtime/test/SharedVulkanRuntimeConfigTest.cpp"], deps = [ ":runtime", "//executorch/runtime/backend:interface", @@ -43,7 +43,7 @@ def define_common_targets(): runtime.cxx_test( name = "shared_gpu_context_registry_test", - srcs = ["runtime/test/SharedGpuContextRegistryTest.cpp"], + srcs = ["runtime/test/SharedVulkanContextRegistryTest.cpp"], deps = [ ":runtime", "//executorch/runtime/core:core", From ece43f3b9708c406908940bb70f2c1d85156c869 Mon Sep 17 00:00:00 2001 From: Elena Zhelezina Date: Mon, 21 Sep 2026 17:44:17 +0100 Subject: [PATCH 4/6] Arm backend: Updated README Signed-off-by: Elena Zhelezina Change-Id: I2d50daa6e477ebbf383abe7c027d578562035746 --- backends/vulkan_shared/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backends/vulkan_shared/README.md b/backends/vulkan_shared/README.md index 6f153d3f151..1708e26007d 100644 --- a/backends/vulkan_shared/README.md +++ b/backends/vulkan_shared/README.md @@ -1,5 +1,8 @@ # Shared Vulkan runtime +This component is not a standalone ExecuTorch backend. It does not +partition graphs, register a backend, or execute delegated operators. + This component is the backend-neutral runtime bridge used by the VGF and ExecuTorch Vulkan delegates. It deliberately does not introduce a unified partitioner or a wrapper delegate. From 737f2afa0d5f792ea8f9b340cb2062552cc63ded Mon Sep 17 00:00:00 2001 From: Elena Zhelezina Date: Mon, 21 Sep 2026 23:08:58 +0100 Subject: [PATCH 5/6] Arm backend: Renamed leftovers. Signed-off-by: Elena Zhelezina Change-Id: I0e7efa4a271e05d80fda0fc76452d6a188371c78 --- backends/vulkan_shared/runtime/test/CMakeLists.txt | 10 +++++----- backends/vulkan_shared/targets.bzl | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backends/vulkan_shared/runtime/test/CMakeLists.txt b/backends/vulkan_shared/runtime/test/CMakeLists.txt index c8e1869948f..2f37279f2e2 100644 --- a/backends/vulkan_shared/runtime/test/CMakeLists.txt +++ b/backends/vulkan_shared/runtime/test/CMakeLists.txt @@ -4,20 +4,20 @@ # LICENSE file in the root directory of this source tree. add_executable( - shared_gpu_runtime_test + shared_vulkan_runtime_test ${CMAKE_CURRENT_SOURCE_DIR}/SharedVulkanContextRegistryTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/SharedVulkanRuntimeConfigTest.cpp ) target_link_libraries( - shared_gpu_runtime_test PRIVATE executorch_vulkan_shared_runtime - Threads::Threads + shared_vulkan_runtime_test PRIVATE executorch_vulkan_shared_runtime + Threads::Threads ) if(TARGET GTest::gtest_main) - target_link_libraries(shared_gpu_runtime_test PRIVATE GTest::gtest_main) + target_link_libraries(shared_vulkan_runtime_test PRIVATE GTest::gtest_main) else() - target_link_libraries(shared_gpu_runtime_test PRIVATE gtest gtest_main) + target_link_libraries(shared_vulkan_runtime_test PRIVATE gtest gtest_main) endif() add_test(NAME shared_gpu_runtime_test COMMAND shared_gpu_runtime_test) diff --git a/backends/vulkan_shared/targets.bzl b/backends/vulkan_shared/targets.bzl index c10f398ea86..886f85a9f68 100644 --- a/backends/vulkan_shared/targets.bzl +++ b/backends/vulkan_shared/targets.bzl @@ -32,7 +32,7 @@ def define_common_targets(): ) runtime.cxx_test( - name = "shared_gpu_runtime_config_test", + name = "shared_vulkan_runtime_config_test", srcs = ["runtime/test/SharedVulkanRuntimeConfigTest.cpp"], deps = [ ":runtime", From f6309b900e8f73ac70b696eec76cbe56d02994ea Mon Sep 17 00:00:00 2001 From: Elena Zhelezina Date: Tue, 22 Sep 2026 11:30:53 +0100 Subject: [PATCH 6/6] Arm backend: One left rename. Signed-off-by: Elena Zhelezina Change-Id: If4db689e22d0fa711b0a6af88dc9b74e0bf1bcd0 --- backends/vulkan_shared/runtime/test/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/vulkan_shared/runtime/test/CMakeLists.txt b/backends/vulkan_shared/runtime/test/CMakeLists.txt index 2f37279f2e2..a4904ad477a 100644 --- a/backends/vulkan_shared/runtime/test/CMakeLists.txt +++ b/backends/vulkan_shared/runtime/test/CMakeLists.txt @@ -20,4 +20,4 @@ else() target_link_libraries(shared_vulkan_runtime_test PRIVATE gtest gtest_main) endif() -add_test(NAME shared_gpu_runtime_test COMMAND shared_gpu_runtime_test) +add_test(NAME shared_vulkan_runtime_test COMMAND shared_vulkan_runtime_test)