Skip to content

Commit 3d3ee36

Browse files
committed
Qualcomm AI Engine Direct - Fix backend register issue on Windows
On Linux, `qnn_executorch_backend.so` is a shared library that links `executorch_core.a` statically. This produces a `.so` that embeds its own copy of `register_backend`, `registered_backends`, and `num_registered_backends`. But those symbols are **ELF global (non-hidden) symbols**. When the runner exe also links `executorch_core.a`, it too has copies of those symbols — but **ELF's symbol interposition rule** says: > When the dynamic linker resolves a symbol at runtime, the executable's definition takes priority over any .so's definition. Even though the `.so` physically contains its own `register_backend` code, at runtime all calls to `register_backend` — including the static initializer inside the `.so` — **are redirected to the exe's copy by the dynamic linker**. Windows has no symbol interposition. Each module — the .dll and the .exe — resolves all statically linked symbols independently, and each carries a completely private copy of any data that came from statically archived code. **The solution is to explicitly bridge the address-space split by passing the exe's own `register_backend` function pointer across the DLL boundary.** The exe's registering function address is passed as `void*` to the DLL's `QnnExecuTorchBackendRegister`. Inside the DLL, the function pointer is a direct address into the exe's code. When the DLL executes `reinterpret_cast<RegisterFn>(register_fn)(backend_msvc)`, the CPU jumps into the exe's `register_backend` function, which writes into the exe's own `registered_backends[]`.
1 parent a6f0cf1 commit 3d3ee36

4 files changed

Lines changed: 46 additions & 0 deletions

File tree

backends/qualcomm/runtime/QnnExecuTorch.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@
3737
#define QNN_EXECUTORCH_EXPORT __attribute__((__visibility__("default")))
3838
#endif
3939

40+
#if defined(_MSC_VER)
41+
#if defined(QNN_EXECUTORCH_BUILDING_DLL)
42+
#define QNN_EXECUTORCH_EXPORT __declspec(dllexport)
43+
#else
44+
#define QNN_EXECUTORCH_EXPORT __declspec(dllimport)
45+
#endif
46+
#else
47+
#define QNN_EXECUTORCH_EXPORT __attribute__((__visibility__("default")))
48+
#endif
49+
4050
#ifdef __cplusplus
4151
extern "C" {
4252
#endif // __cplusplus
@@ -92,6 +102,9 @@ QNN_EXECUTORCH_EXPORT void QnnExecuTorchAddCustomMemTensorAddr(
92102
/// Free the allocated shared memory.
93103
QNN_EXECUTORCH_EXPORT void QnnExecuTorchFreeCustomMem(void* buffer_ptr);
94104

105+
/// Register the QnnBackend with the ExecuTorch runtime.
106+
QNN_EXECUTORCH_EXPORT void QnnExecuTorchBackendRegister(void* register_fn);
107+
95108
#ifdef __cplusplus
96109
}
97110
#endif // __cplusplus

backends/qualcomm/runtime/QnnExecuTorchBackend.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,37 @@ void QnnExecuTorchBackend::erase_cached_delegate(
351351
}
352352

353353
namespace {
354+
#if !defined(_MSC_VER)
354355
auto cls = QnnExecuTorchBackend();
355356
executorch::runtime::Backend backend{QNN_BACKEND, &cls};
357+
// On non-Windows platforms a single copy of executorch_core is shared between
358+
// the DLL and the exe, so the static initializer writes into the correct
359+
// registered_backends[] array.
356360
static auto success_with_compiler = register_backend(backend);
361+
#endif
357362
} // namespace
363+
364+
#if defined(_MSC_VER)
365+
static QnnExecuTorchBackend cls_msvc;
366+
static executorch::runtime::Backend backend_msvc{QNN_BACKEND, &cls_msvc};
367+
#endif
368+
358369
} // namespace qnn
359370
} // namespace backends
360371
} // namespace executorch
372+
373+
#if defined(_MSC_VER)
374+
// On Windows the DLL and the exe each carry a private copy of executorch_core
375+
// globals. The runner calls this function at startup, passing its own
376+
// register_backend() so registration happens in the exe's address space.
377+
void QnnExecuTorchBackendRegister(void* register_fn) {
378+
using RegisterFn = executorch::runtime::Error (*)(
379+
const executorch::runtime::Backend&);
380+
reinterpret_cast<RegisterFn>(register_fn)(
381+
executorch::backends::qnn::backend_msvc);
382+
}
383+
#else
384+
void QnnExecuTorchBackendRegister(void*) {
385+
// No-op
386+
}
387+
#endif

examples/qualcomm/executor_runner/qnn_executor_runner.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ class CustomMemory {
208208

209209
int main(int argc, char** argv) {
210210
executorch::runtime::runtime_init();
211+
QnnExecuTorchBackendRegister(
212+
reinterpret_cast<void*>(executorch::runtime::register_backend));
211213

212214
gflags::ParseCommandLineFlags(&argc, &argv, true);
213215
if (argc != 1) {

examples/qualcomm/oss_scripts/whisper/qnn_whisper_runner.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
*
1414
*/
1515

16+
#include <executorch/backends/qualcomm/runtime/QnnExecuTorch.h>
1617
#include <executorch/examples/qualcomm/oss_scripts/whisper/runner/runner.h>
18+
#include <executorch/runtime/backend/interface.h>
1719
#include <executorch/runtime/platform/log.h>
1820
#include <gflags/gflags.h>
1921
#include <fstream>
@@ -95,6 +97,8 @@ std::vector<std::vector<std::vector<char>>> parse_input_list_file(
9597
}
9698

9799
int main(int argc, char** argv) {
100+
QnnExecuTorchBackendRegister(
101+
reinterpret_cast<void*>(executorch::runtime::register_backend));
98102
gflags::ParseCommandLineFlags(&argc, &argv, true);
99103
// create llama runner
100104
example::Runner runner(FLAGS_model_path, FLAGS_tokenizer_json_path);

0 commit comments

Comments
 (0)