Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,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/vulkan_shared)
endif()

if(EXECUTORCH_BUILD_ARM_BAREMETAL
OR EXECUTORCH_BUILD_ARM_ETHOSU_LINUX
Expand Down
11 changes: 11 additions & 0 deletions backends/vulkan_shared/BUCK
Original file line number Diff line number Diff line change
@@ -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,)
97 changes: 97 additions & 0 deletions backends/vulkan_shared/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 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(VULKAN_SHARED_HEADERS_PATH
${EXECUTORCH_ROOT}/backends/vulkan/third-party/Vulkan-Headers
)

if(NOT EXISTS "${VULKAN_SHARED_HEADERS_PATH}/include/vulkan/vulkan.h")
message(
FATAL_ERROR
"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()

# 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_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
)

target_compile_definitions(
executorch_vulkan_shared_runtime PRIVATE EXECUTORCH_VULKAN_SHARED_BUILDING
)

target_include_directories(
executorch_vulkan_shared_runtime
PUBLIC $<BUILD_INTERFACE:${EXECUTORCH_ROOT}/..>
$<BUILD_INTERFACE:${EXECUTORCH_ROOT}/runtime/core/portable_type/c10>
$<BUILD_INTERFACE:${VULKAN_SHARED_HEADERS_PATH}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

target_link_libraries(
executorch_vulkan_shared_runtime
PUBLIC executorch_core
PRIVATE Threads::Threads
)

set_target_properties(
executorch_vulkan_shared_runtime
PROPERTIES CXX_STANDARD 17
CXX_STANDARD_REQUIRED YES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)

install(
TARGETS executorch_vulkan_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/vulkan_shared/runtime
FILES_MATCHING
PATTERN "*.h"
PATTERN "test" EXCLUDE
)

# SharedVulkanContext.h is a public installed header and includes
# <vulkan/vulkan.h>. 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 ${VULKAN_SHARED_HEADERS_PATH}/include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

if(EXECUTORCH_BUILD_TESTS)
add_subdirectory(runtime/test)
endif()
64 changes: 64 additions & 0 deletions backends/vulkan_shared/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# 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
Comment thread
wwwind marked this conversation as resolved.
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 |
| --- | --- | --- | --- |
| `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

`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 `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 `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
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
`SharedVulkanContext::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.
64 changes: 64 additions & 0 deletions backends/vulkan_shared/runtime/SharedVulkanContext.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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 <executorch/backends/vulkan_shared/runtime/SharedVulkanContext.h>

#include <algorithm>
#include <utility>

namespace executorch {
namespace backends {
namespace vulkan_shared {

SharedVulkanContext::SharedVulkanContext(
SharedVulkanContextCreateInfo create_info)
: create_info_(std::move(create_info)) {}

SharedVulkanContext::~SharedVulkanContext() = default;

const SharedVulkanContextKey& SharedVulkanContext::key() const {
return create_info_.key;
}

VkInstance SharedVulkanContext::instance() const {
return create_info_.instance;
}

VkPhysicalDevice SharedVulkanContext::physical_device() const {
return create_info_.physical_device;
}

VkDevice SharedVulkanContext::device() const {
return create_info_.device;
}

uint32_t SharedVulkanContext::queue_family_index() const {
return create_info_.queue_family_index;
}

bool SharedVulkanContext::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 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 &&
create_info_.queue != VK_NULL_HANDLE &&
create_info_.queue_family_index != std::numeric_limits<uint32_t>::max() &&
create_info_.lifetime_anchor != nullptr;
}

} // namespace vulkan_shared
} // namespace backends
} // namespace executorch
100 changes: 100 additions & 0 deletions backends/vulkan_shared/runtime/SharedVulkanContext.h
Original file line number Diff line number Diff line change
@@ -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 <executorch/backends/vulkan_shared/runtime/export.h>

#include <vulkan/vulkan.h>

#include <cstdint>
#include <limits>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace executorch {
namespace backends {
namespace vulkan_shared {

struct SharedVulkanContextKey final {
std::string context_name;
int group_id = 0;

bool valid() const {
return !context_name.empty();
}

friend bool operator==(
const SharedVulkanContextKey& lhs,
const SharedVulkanContextKey& rhs) {
return lhs.group_id == rhs.group_id && lhs.context_name == rhs.context_name;
}

friend bool operator!=(
const SharedVulkanContextKey& lhs,
const SharedVulkanContextKey& 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 SharedVulkanContext reference is released. The
// anchor destructor may perform backend/application Vulkan teardown.
struct SharedVulkanContextCreateInfo final {
SharedVulkanContextKey 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<uint32_t>::max();
std::vector<std::string> enabled_device_extensions;
std::shared_ptr<void> lifetime_anchor;
};

class EXECUTORCH_VULKAN_SHARED_API SharedVulkanContext final {
public:
explicit SharedVulkanContext(SharedVulkanContextCreateInfo create_info);
~SharedVulkanContext();

SharedVulkanContext(const SharedVulkanContext&) = delete;
SharedVulkanContext& operator=(const SharedVulkanContext&) = delete;

const SharedVulkanContextKey& 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 <typename Fn>
decltype(auto) with_locked_queue(Fn&& fn) const {
std::lock_guard<std::mutex> lock(queue_mutex_);
return std::forward<Fn>(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:
SharedVulkanContextCreateInfo create_info_;
mutable std::mutex queue_mutex_;
};

using SharedVulkanContextPtr = std::shared_ptr<SharedVulkanContext>;

} // namespace vulkan_shared
} // namespace backends
} // namespace executorch
Loading
Loading