Skip to content

Commit addc812

Browse files
authored
[QNN EP] Default to HTP instead of using last provided device (microsoft#26696)
### Description When multiple devices are provided in `AppendExecutionProviders_V2`, default to the NPU device, instead of picking the last device in the list.
1 parent ce4a7c5 commit addc812

2 files changed

Lines changed: 174 additions & 68 deletions

File tree

onnxruntime/core/providers/qnn/qnn_provider_factory.cc

Lines changed: 98 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@
77
#include "core/providers/qnn/qnn_provider_factory_creator.h"
88
#include "core/providers/qnn/qnn_execution_provider.h"
99
#include "core/providers/qnn/builder/qnn_utils.h"
10+
#include "core/session/abi_devices.h"
11+
12+
static const std::unordered_map<OrtHardwareDeviceType, std::string> kDefaultBackends = {
13+
#if defined(_WIN32)
14+
{OrtHardwareDeviceType_NPU, "QnnHtp.dll"},
15+
{OrtHardwareDeviceType_GPU, "QnnGpu.dll"},
16+
#else
17+
{OrtHardwareDeviceType_NPU, "libQnnHtp.so"},
18+
{OrtHardwareDeviceType_GPU, "libQnnGpu.so"},
19+
#endif
20+
};
1021

1122
namespace onnxruntime {
1223
struct QNNProviderFactory : IExecutionProviderFactory {
@@ -61,6 +72,35 @@ std::shared_ptr<IExecutionProviderFactory> QNNProviderFactoryCreator::Create(con
6172
return std::make_shared<onnxruntime::QNNProviderFactory>(provider_options_map, config_options);
6273
}
6374
#else
75+
/// @brief Gets the path of directory containing the dynamic library that contains the address.
76+
/// @param address An address of a function or variable in the dynamic library.
77+
/// @return The path of the directory containing the dynamic library, or an empty string if the path cannot be determined.
78+
static onnxruntime::PathString GetDynamicLibraryLocationByAddress(const void* address) {
79+
#ifdef _WIN32
80+
HMODULE moduleHandle;
81+
if (!::GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
82+
reinterpret_cast<LPCWSTR>(address), &moduleHandle)) {
83+
return {};
84+
}
85+
std::wstring buffer;
86+
for (std::uint32_t size{70}; size < 4096; size *= 2) {
87+
buffer.resize(size, L'\0');
88+
const std::uint32_t requiredSize = ::GetModuleFileNameW(moduleHandle, buffer.data(), size);
89+
if (requiredSize == 0) {
90+
break;
91+
}
92+
if (requiredSize == size) {
93+
continue;
94+
}
95+
buffer.resize(requiredSize);
96+
return {std::move(buffer)};
97+
}
98+
#else
99+
std::ignore = address;
100+
#endif
101+
return {};
102+
}
103+
64104
struct QNN_Provider : Provider {
65105
std::shared_ptr<IExecutionProviderFactory> CreateExecutionProviderFactory(const void* param) override {
66106
if (param == nullptr) {
@@ -80,13 +120,61 @@ struct QNN_Provider : Provider {
80120
return std::make_shared<onnxruntime::QNNProviderFactory>(*provider_options, config_options);
81121
}
82122

83-
Status CreateIExecutionProvider(const OrtHardwareDevice* const* /*devices*/,
123+
Status CreateIExecutionProvider(const OrtHardwareDevice* const* devices,
84124
const OrtKeyValuePairs* const* /*ep_metadata*/,
85-
size_t /*num_devices*/,
125+
size_t num_devices,
86126
ProviderOptions& provider_options,
87127
const OrtSessionOptions& session_options,
88128
const OrtLogger& logger,
89129
std::unique_ptr<IExecutionProvider>& ep) override {
130+
if (provider_options.find("backend_path") == provider_options.end() &&
131+
provider_options.find("backend_type") == provider_options.end()) {
132+
// If neither "backend_path" nor "backend_type" has been given in the provider options, then determine the backend based
133+
// on the provided devices. As QNN EP does not support partitioning across backends, if multiple devices are provided,
134+
// default to HTP (if present) or else to the GPU.
135+
const OrtHardwareDevice* device_to_use = nullptr;
136+
if (num_devices == 0) {
137+
return Status(common::ONNXRUNTIME, ORT_EP_FAIL, "No devices were provided to QNN EP.");
138+
} else if (num_devices == 1) {
139+
device_to_use = devices[0];
140+
} else {
141+
const auto is_npu = [](const OrtHardwareDevice* device) { return device->type == OrtHardwareDeviceType_NPU; };
142+
const auto is_gpu = [](const OrtHardwareDevice* device) { return device->type == OrtHardwareDeviceType_GPU; };
143+
144+
auto device_it = std::find_if(devices, devices + num_devices, is_npu);
145+
if (device_it != devices + num_devices) {
146+
LOGS_DEFAULT(WARNING) << "QNN EP only supports one device. Only the NPU device will be used.";
147+
device_to_use = *device_it;
148+
} else {
149+
device_it = std::find_if(devices, devices + num_devices, is_gpu);
150+
if (device_it != devices + num_devices) {
151+
LOGS_DEFAULT(WARNING)
152+
<< "QNN EP only supports one device. An NPU device was not provided, so only the GPU device will be used.";
153+
device_to_use = *device_it;
154+
} else {
155+
return Status(common::ONNXRUNTIME, ORT_EP_FAIL,
156+
"Multiple devices were provided to QNN EP, but neither an NPU nor a GPU was included.");
157+
}
158+
}
159+
}
160+
ORT_RETURN_IF(device_to_use == nullptr, "Failed to select device for QNN EP!");
161+
162+
auto default_backends_it = kDefaultBackends.find(device_to_use->type);
163+
ORT_RETURN_IF(default_backends_it == kDefaultBackends.end(),
164+
"Could not determine default backend path for device of type: ", device_to_use->type);
165+
166+
// Identify the path of the current dynamic library, and expect that the backend library is in the same directory.
167+
onnxruntime::PathString current_path = GetDynamicLibraryLocationByAddress(
168+
reinterpret_cast<const void*>(&kDefaultBackends));
169+
170+
std::filesystem::path parent_path;
171+
if (!current_path.empty()) {
172+
parent_path = std::filesystem::path{std::move(current_path)}.parent_path();
173+
}
174+
175+
provider_options["backend_path"] = (parent_path / default_backends_it->second).string();
176+
}
177+
90178
const ConfigOptions* config_options = &session_options.GetConfigOptions();
91179

92180
std::array<const void*, 2> configs_array = {&provider_options, config_options};
@@ -115,45 +203,14 @@ ORT_API(onnxruntime::Provider*, GetProvider) {
115203
#include "core/framework/error_code_helper.h"
116204
#include "onnxruntime_config.h" // for ORT_VERSION
117205

118-
/// @brief Gets the path of directory containing the dynamic library that contains the address.
119-
/// @param address An address of a function or variable in the dynamic library.
120-
/// @return The path of the directory containing the dynamic library, or an empty string if the path cannot be determined.
121-
static onnxruntime::PathString GetDynamicLibraryLocationByAddress(const void* address) {
122-
#ifdef _WIN32
123-
HMODULE moduleHandle;
124-
if (!::GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
125-
reinterpret_cast<LPCWSTR>(address), &moduleHandle)) {
126-
return {};
127-
}
128-
std::wstring buffer;
129-
for (std::uint32_t size{70}; size < 4096; size *= 2) {
130-
buffer.resize(size, L'\0');
131-
const std::uint32_t requiredSize = ::GetModuleFileNameW(moduleHandle, buffer.data(), size);
132-
if (requiredSize == 0) {
133-
break;
134-
}
135-
if (requiredSize == size) {
136-
continue;
137-
}
138-
buffer.resize(requiredSize);
139-
return {std::move(buffer)};
140-
}
141-
#else
142-
std::ignore = address;
143-
#endif
144-
return {};
145-
}
146-
147206
// OrtEpApi infrastructure to be able to use the QNN EP as an OrtEpFactory for auto EP selection.
148207
struct QnnEpFactory : OrtEpFactory {
149208
QnnEpFactory(const OrtApi& ort_api_in,
150209
const OrtLogger& default_logger_in,
151-
const char* ep_name,
152-
std::unordered_map<OrtHardwareDeviceType, std::string> supported_backends)
210+
const char* ep_name)
153211
: ort_api{ort_api_in},
154212
default_logger{default_logger_in},
155-
ep_name{ep_name},
156-
supported_backends{std::move(supported_backends)} {
213+
ep_name{ep_name} {
157214
ort_version_supported = ORT_API_VERSION;
158215
GetName = GetNameImpl;
159216
GetVendor = GetVendorImpl;
@@ -195,27 +252,24 @@ struct QnnEpFactory : OrtEpFactory {
195252
// An EP created with this factory is expected to be able to execute a model with *all* supported
196253
// hardware devices at once. A single instance of QNN EP is not currently setup to partition a model among
197254
// multiple different QNN backends at once (e.g, npu, cpu, gpu), so currently this factory instance is set
198-
// to pick the last specified backend only.
255+
// to default to npu.
199256
static OrtStatus* GetSupportedDevicesImpl(OrtEpFactory* this_ptr,
200257
const OrtHardwareDevice* const* devices,
201258
size_t num_devices,
202259
OrtEpDevice** ep_devices,
203260
size_t max_ep_devices,
204261
size_t* p_num_ep_devices) noexcept {
205262
size_t& num_ep_devices = *p_num_ep_devices;
263+
num_ep_devices = 0;
264+
206265
auto* factory = static_cast<QnnEpFactory*>(this_ptr);
207266

208267
for (size_t i = 0; i < num_devices && num_ep_devices < max_ep_devices; ++i) {
209268
const OrtHardwareDevice& device = *devices[i];
210-
auto supported_backend_it = factory->supported_backends.find(factory->ort_api.HardwareDevice_Type(&device));
211-
if (supported_backend_it != factory->supported_backends.end() &&
269+
if (kDefaultBackends.find(factory->ort_api.HardwareDevice_Type(&device)) != kDefaultBackends.end() &&
212270
factory->ort_api.HardwareDevice_VendorId(&device) == factory->vendor_id) {
213-
OrtKeyValuePairs* ep_options = nullptr;
214-
factory->ort_api.CreateKeyValuePairs(&ep_options);
215-
factory->ort_api.AddKeyValuePair(ep_options, "backend_path", supported_backend_it->second.c_str());
216-
OrtStatus* status = factory->ort_api.GetEpApi()->CreateEpDevice(factory, &device, nullptr, ep_options,
271+
OrtStatus* status = factory->ort_api.GetEpApi()->CreateEpDevice(factory, &device, nullptr, nullptr,
217272
&ep_devices[num_ep_devices++]);
218-
factory->ort_api.ReleaseKeyValuePairs(ep_options);
219273
ORT_API_RETURN_IF_ERROR(status);
220274
}
221275
}
@@ -282,8 +336,6 @@ struct QnnEpFactory : OrtEpFactory {
282336

283337
// Qualcomm vendor ID. Refer to the ACPI ID registry (search Qualcomm): https://uefi.org/ACPI_ID_List
284338
const uint32_t vendor_id{'Q' | ('C' << 8) | ('O' << 16) | ('M' << 24)};
285-
const std::unordered_map<OrtHardwareDeviceType, std::string> supported_backends; // Supported OrtHardwareDeviceTypes
286-
// and their QNN backend paths
287339
};
288340

289341
extern "C" {
@@ -295,35 +347,13 @@ OrtStatus* CreateEpFactories(const char* /*registration_name*/, const OrtApiBase
295347
OrtEpFactory** factories, size_t max_factories, size_t* num_factories) {
296348
const OrtApi* ort_api = ort_api_base->GetApi(ORT_API_VERSION);
297349

298-
std::unordered_map<OrtHardwareDeviceType, std::string> supported_backends = {
299-
#if defined(_WIN32)
300-
{OrtHardwareDeviceType_NPU, "QnnHtp.dll"},
301-
{OrtHardwareDeviceType_GPU, "QnnGpu.dll"},
302-
#else
303-
{OrtHardwareDeviceType_NPU, "libQnnHtp.so"},
304-
{OrtHardwareDeviceType_GPU, "libQnnGpu.so"},
305-
#endif
306-
};
307-
308-
for (auto& [_, backend_path] : supported_backends) {
309-
// Identify the path of the current dynamic library, and expect that backend_path is in the same directory.
310-
onnxruntime::PathString current_path = GetDynamicLibraryLocationByAddress(
311-
reinterpret_cast<const void*>(&CreateEpFactories));
312-
313-
if (!current_path.empty()) {
314-
const std::filesystem::path parent_path = std::filesystem::path{std::move(current_path)}.parent_path();
315-
backend_path = (parent_path / backend_path).string();
316-
}
317-
}
318-
319350
if (max_factories < 1) {
320351
return ort_api->CreateStatus(ORT_INVALID_ARGUMENT,
321352
"Not enough space to return EP factory. Need at least one.");
322353
}
323354

324355
auto factory = std::make_unique<QnnEpFactory>(*ort_api, *default_logger,
325-
onnxruntime::kQnnExecutionProvider,
326-
supported_backends);
356+
onnxruntime::kQnnExecutionProvider);
327357
factories[0] = factory.release();
328358
*num_factories = 1;
329359

onnxruntime/test/providers/qnn/qnn_basic_test.cc

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,6 +1497,82 @@ TEST_F(QnnGPUBackendTests, AutoEp_PreferGpu) {
14971497

14981498
ASSERT_ORTSTATUS_OK(Ort::GetApi().UnregisterExecutionProviderLibrary(*ort_env, kQnnExecutionProvider));
14991499
}
1500+
1501+
TEST_F(QnnHTPBackendTests, AutoEp_AllDevices) {
1502+
ASSERT_ORTSTATUS_OK(Ort::GetApi().RegisterExecutionProviderLibrary(*ort_env, kQnnExecutionProvider,
1503+
ORT_TSTR("onnxruntime_providers_qnn.dll")));
1504+
1505+
Ort::SessionOptions so;
1506+
auto devices = ort_env->GetEpDevices();
1507+
std::vector<Ort::ConstEpDevice> selected_devices;
1508+
1509+
std::copy_if(devices.begin(),
1510+
devices.end(),
1511+
std::back_inserter(selected_devices),
1512+
[](Ort::ConstEpDevice& d) { return std::string_view(d.EpName()) == kQnnExecutionProvider; });
1513+
1514+
ASSERT_TRUE(selected_devices.size() > 0) << "No QNN devices were found.";
1515+
1516+
so.AppendExecutionProvider_V2(*ort_env, selected_devices, {});
1517+
1518+
const ORTCHAR_T* ort_model_path = ORT_MODEL_FOLDER "nhwc_resize_sizes_opset18.quant.onnx";
1519+
Ort::Session session(*ort_env, ort_model_path, so);
1520+
EXPECT_TRUE(SessionHasEp(session, kQnnExecutionProvider));
1521+
1522+
ASSERT_ORTSTATUS_OK(Ort::GetApi().UnregisterExecutionProviderLibrary(*ort_env, kQnnExecutionProvider));
1523+
}
1524+
1525+
TEST_F(QnnHTPBackendTests, AutoEp_NpuOnly) {
1526+
ASSERT_ORTSTATUS_OK(Ort::GetApi().RegisterExecutionProviderLibrary(*ort_env, kQnnExecutionProvider,
1527+
ORT_TSTR("onnxruntime_providers_qnn.dll")));
1528+
1529+
Ort::SessionOptions so;
1530+
auto devices = ort_env->GetEpDevices();
1531+
std::vector<Ort::ConstEpDevice> selected_devices;
1532+
1533+
std::copy_if(devices.begin(),
1534+
devices.end(),
1535+
std::back_inserter(selected_devices),
1536+
[](Ort::ConstEpDevice& d) {
1537+
return std::string_view(d.EpName()) == kQnnExecutionProvider && d.Device().Type() == OrtHardwareDeviceType_NPU;
1538+
});
1539+
1540+
ASSERT_TRUE(selected_devices.size() > 0) << "No QNN NPU device was found.";
1541+
1542+
so.AppendExecutionProvider_V2(*ort_env, selected_devices, {});
1543+
1544+
const ORTCHAR_T* ort_model_path = ORT_MODEL_FOLDER "nhwc_resize_sizes_opset18.quant.onnx";
1545+
Ort::Session session(*ort_env, ort_model_path, so);
1546+
EXPECT_TRUE(SessionHasEp(session, kQnnExecutionProvider));
1547+
1548+
ASSERT_ORTSTATUS_OK(Ort::GetApi().UnregisterExecutionProviderLibrary(*ort_env, kQnnExecutionProvider));
1549+
}
1550+
1551+
TEST_F(QnnGPUBackendTests, AutoEp_GpuOnly) {
1552+
ASSERT_ORTSTATUS_OK(Ort::GetApi().RegisterExecutionProviderLibrary(*ort_env, kQnnExecutionProvider,
1553+
ORT_TSTR("onnxruntime_providers_qnn.dll")));
1554+
1555+
Ort::SessionOptions so;
1556+
auto devices = ort_env->GetEpDevices();
1557+
std::vector<Ort::ConstEpDevice> selected_devices;
1558+
1559+
std::copy_if(devices.begin(),
1560+
devices.end(),
1561+
std::back_inserter(selected_devices),
1562+
[](Ort::ConstEpDevice& d) {
1563+
return std::string_view(d.EpName()) == kQnnExecutionProvider && d.Device().Type() == OrtHardwareDeviceType_GPU;
1564+
});
1565+
1566+
ASSERT_TRUE(selected_devices.size() > 0) << "No QNN GPU device was found.";
1567+
1568+
so.AppendExecutionProvider_V2(*ort_env, selected_devices, {});
1569+
1570+
const ORTCHAR_T* ort_model_path = ORT_MODEL_FOLDER "nhwc_resize_sizes_opset18.onnx";
1571+
Ort::Session session(*ort_env, ort_model_path, so);
1572+
EXPECT_TRUE(SessionHasEp(session, kQnnExecutionProvider));
1573+
1574+
ASSERT_ORTSTATUS_OK(Ort::GetApi().UnregisterExecutionProviderLibrary(*ort_env, kQnnExecutionProvider));
1575+
}
15001576
#endif // defined(WIN32) && !BUILD_QNN_EP_STATIC_LIB
15011577

15021578
// Test whether QNN EP can handle the case where the number of graph inputs and

0 commit comments

Comments
 (0)