diff --git a/backends/arm/runtime/VGFBackend.cpp b/backends/arm/runtime/VGFBackend.cpp index c61ffb24390..cfec706b8b9 100644 --- a/backends/arm/runtime/VGFBackend.cpp +++ b/backends/arm/runtime/VGFBackend.cpp @@ -88,7 +88,14 @@ void vkml_free_basics( class VGFBackend final : public ::executorch::runtime::BackendInterface { public: - VGFBackend() { + VGFBackend() = default; + + // Lazy Vulkan init — runs on first use, not in the constructor. + void ensure_initialized() { + if (is_initialized_) { + return; + } + VkResult result; // Fetch basic vulkan objects once @@ -122,6 +129,7 @@ class VGFBackend final : public ::executorch::runtime::BackendInterface { bool is_available() const override { ET_LOG(Info, "Checking VGFBackend is available"); + const_cast(this)->ensure_initialized(); if (!is_initialized_) { return false; } @@ -134,6 +142,7 @@ class VGFBackend final : public ::executorch::runtime::BackendInterface { ArrayRef compile_specs) const override { ET_LOG(Info, "Entered VGF init"); + const_cast(this)->ensure_initialized(); if (!is_initialized_) { ET_LOG( Error, @@ -334,6 +343,16 @@ VkResult vkml_allocate_basics( } volkLoadInstance(*instance); + // Bail out if the driver lacks ARM tensor/datagraph extensions. + if (!vkCreateTensorARM) { + ET_LOG( + Error, + "Vulkan driver does not support ARM tensor extensions (VK_ARM_tensors)"); + vkDestroyInstance(*instance, nullptr); + *instance = VK_NULL_HANDLE; + return VK_ERROR_FEATURE_NOT_PRESENT; + } + // Pick first GPU uint32_t gpu_count = 0; vkEnumeratePhysicalDevices(*instance, &gpu_count, nullptr); diff --git a/backends/arm/runtime/VGFSetup.cpp b/backends/arm/runtime/VGFSetup.cpp index fd3a114c190..da9e4f46865 100644 --- a/backends/arm/runtime/VGFSetup.cpp +++ b/backends/arm/runtime/VGFSetup.cpp @@ -538,7 +538,7 @@ bool VgfRepr::process_vgf(const char* vgf_data, ArrayRef specs) { .pNext = nullptr, .flags = 0, .bindingCount = static_cast(layout_bindings.size()), - layout_bindings.data(), + .pBindings = layout_bindings.data(), }; result = vkCreateDescriptorSetLayout(vk_device, &layout_info, nullptr, &vk_layout); diff --git a/backends/arm/runtime/targets.bzl b/backends/arm/runtime/targets.bzl index ec6993ff721..c727b81b0d1 100644 --- a/backends/arm/runtime/targets.bzl +++ b/backends/arm/runtime/targets.bzl @@ -32,3 +32,35 @@ def define_common_targets(): "fbsource//third-party/ethos-u-core-driver:core_driver", ], ) + runtime.cxx_library( + name = "vgf_backend", + srcs = [ + "VGFBackend.cpp", + "VGFSetup.cpp", + # Volk must be compiled directly into this target so its global + # function-pointer variables live in the same linkage unit. + # Linking from a separate static library causes the linker to + # drop the symbols when building a shared library. + "fbsource//third-party/vulkan-headers-1.4.343/v1.4.343/src:volk_arm_src", + ], + exported_headers = ["VGFSetup.h"], + # @lint-ignore BUCKLINT: Avoid `link_whole=True` (https://fburl.com/avoid-link-whole) + link_whole = True, + supports_python_dlopen = True, + compiler_flags = [ + "-Wno-global-constructors", + "-Wno-header-hygiene", + "-Wno-unused-variable", + "-Wno-missing-field-initializers", + "-DUSE_VULKAN_WRAPPER", + "-DUSE_VULKAN_VOLK", + ], + visibility = ["PUBLIC"], + deps = [ + "//executorch/runtime/backend:interface", + "//executorch/runtime/core:core", + "fbsource//third-party/arm-vgf-library/v0.8.0/src:vgf", + "fbsource//third-party/vulkan-headers-1.4.343/v1.4.343/src:volk_arm", + "fbsource//third-party/vulkan-headers-1.4.343/v1.4.343/src:vulkan-headers", + ], + ) diff --git a/backends/arm/test/runner_utils.py b/backends/arm/test/runner_utils.py index 226ed94fb8a..914a95f0c8d 100644 --- a/backends/arm/test/runner_utils.py +++ b/backends/arm/test/runner_utils.py @@ -35,7 +35,10 @@ from executorch.backends.arm.tosa.compile_spec import TosaCompileSpec from executorch.backends.arm.tosa.specification import Tosa_1_00, TosaSpecification from executorch.backends.arm.vgf import VgfCompileSpec -from executorch.backends.arm.vgf.model_converter import find_model_converter_binary +from executorch.backends.arm.vgf.model_converter import ( + find_model_converter_binary, + model_converter_env, +) from executorch.exir import ExecutorchProgramManager, ExportedProgram from executorch.exir.lowered_backend_module import LoweredBackendModule from torch.fx.node import Node @@ -818,7 +821,7 @@ def model_converter_installed() -> bool: return False try: - _run_cmd([model_converter, "--version"], check=True) + _run_cmd([model_converter, "--version"], check=True, env=model_converter_env()) except Exception: return False diff --git a/backends/arm/test/targets.bzl b/backends/arm/test/targets.bzl index 5c5f9dd02c3..047ad7d57f5 100644 --- a/backends/arm/test/targets.bzl +++ b/backends/arm/test/targets.bzl @@ -67,9 +67,21 @@ def define_arm_tests(): resources = ["conftest.py"], compile = "with-source", typing = False, + env = {} if runtime.is_oss else { + "MODEL_CONVERTER_PATH": "$(location fbsource//third-party/pypi/ai-ml-sdk-model-converter/0.8.0:model-converter-bin)", + "MODEL_CONVERTER_LIB_DIR": "$(location fbsource//third-party/nvidia-nsight-systems:linux-x86_64)/host-linux-x64", + "LAVAPIPE_LIB_PATH": "$(location fbsource//third-party/mesa/src/gallium/frontends/lavapipe:vulkan_lvp)", + "EMULATION_LAYER_TENSOR_SO": "$(location fbsource//third-party/arm-ml-emulation-layer/v0.9.0/src:libVkLayer_Tensor)", + "EMULATION_LAYER_GRAPH_SO": "$(location fbsource//third-party/arm-ml-emulation-layer/v0.9.0/src:libVkLayer_Graph)", + "EMULATION_LAYER_TENSOR_JSON": "$(location fbsource//third-party/arm-ml-emulation-layer/v0.9.0/src:VkLayer_Tensor_json)", + "EMULATION_LAYER_GRAPH_JSON": "$(location fbsource//third-party/arm-ml-emulation-layer/v0.9.0/src:VkLayer_Graph_json)", + }, preload_deps = [ "//executorch/kernels/quantized:custom_ops_generated_lib", - ], + ] + ([] if runtime.is_oss else [ + "fbsource//third-party/khronos:vulkan", + "//executorch/backends/arm/runtime:vgf_backend", + ]), deps = [ "//executorch/backends/arm/test:arm_tester" if runtime.is_oss else "//executorch/backends/arm/test/tester/fb:arm_tester_fb", "//executorch/backends/arm/test:conftest", diff --git a/backends/arm/vgf/backend.py b/backends/arm/vgf/backend.py index 563b4e980b9..e03b498a160 100644 --- a/backends/arm/vgf/backend.py +++ b/backends/arm/vgf/backend.py @@ -28,6 +28,7 @@ VgfCompileSpec, ) from executorch.backends.arm.vgf.model_converter import ( # type: ignore[import-not-found] + model_converter_env, require_model_converter_binary, ) from executorch.exir.backend.backend_details import ( # type: ignore[import-not-found] @@ -157,7 +158,11 @@ def vgf_compile( ] try: subprocess.run( # nosec B602, B603 - shell invocation constrained to trusted converter binary with trusted inputs - conversion_command, shell=False, check=True, capture_output=True + conversion_command, + shell=False, + check=True, + capture_output=True, + env=model_converter_env(), ) except subprocess.CalledProcessError as process_error: conversion_command_str = " ".join(conversion_command) diff --git a/backends/arm/vgf/model_converter.py b/backends/arm/vgf/model_converter.py index dffbf76f26a..2d3868837b1 100644 --- a/backends/arm/vgf/model_converter.py +++ b/backends/arm/vgf/model_converter.py @@ -5,6 +5,7 @@ from __future__ import annotations +import os from shutil import which from typing import Optional @@ -13,7 +14,10 @@ def find_model_converter_binary() -> Optional[str]: - """Return the name of the first model converter executable on PATH.""" + """Return the path/name of the first model converter executable found.""" + env_path = os.environ.get("MODEL_CONVERTER_PATH") + if env_path and os.path.isfile(env_path) and os.access(env_path, os.X_OK): + return env_path for candidate in (MODEL_CONVERTER_BINARY, _MODEL_CONVERTER_FALLBACK_BINARY): if which(candidate): @@ -21,6 +25,22 @@ def find_model_converter_binary() -> Optional[str]: return None +def model_converter_env() -> dict[str, str]: + """Return an env dict suitable for running model-converter as a subprocess. + + If MODEL_CONVERTER_LIB_DIR is set, it is prepended to LD_LIBRARY_PATH so the + binary can find a compatible libstdc++ (or other shared libs) without + polluting the parent process environment. + + """ + env = dict(os.environ) + lib_dir = env.pop("MODEL_CONVERTER_LIB_DIR", None) + if lib_dir: + existing = env.get("LD_LIBRARY_PATH", "") + env["LD_LIBRARY_PATH"] = f"{lib_dir}:{existing}" if existing else lib_dir + return env + + def require_model_converter_binary() -> str: """Return a usable model converter executable or raise a helpful error."""