Skip to content

Commit 5c7ba13

Browse files
authored
fix: Add Linux NPU discovery through sysfs accel devices (#28703)
## Description This PR adds Linux NPU discovery through sysfs accel devices Currently, `DeviceDiscovery::DiscoverDevicesForPlatform()` on Linux discovers CPU and GPU devices, but NPU discovery is still missing. As a result, plugin execution providers that filter devices by `OrtHardwareDeviceType_NPU` do not receive any NPU hardware devices on Linux, even when the NPU is present and exposed by the kernel. This change scans `/sys/class/accel` for `accelN` devices and creates `OrtHardwareDevice` entries with: - `type = OrtHardwareDeviceType_NPU` - PCI `vendor_id` - PCI `device_id` - `accel_idx` metadata - `pci_bus_id` metadata when available This enables Linux systems with NPUs exposed through the accel subsystem, such as AMD Ryzen AI / XDNA devices, to be reported through ORT device discovery and made available to plugin EP factories. ## Changes - Add Linux sysfs discovery for NPU devices under `/sys/class/accel`. - Read NPU PCI vendor and device IDs from the underlying sysfs device path. - Add NPU metadata including `accel_idx` and `pci_bus_id`. - Include discovered NPU devices in `DeviceDiscovery::DiscoverDevicesForPlatform()`. - Add a `kSysfsAccelPath` constant for the Linux accel sysfs path. ## Motivation Linux plugin EPs that target NPUs rely on ORT passing `OrtHardwareDeviceType_NPU` devices into `GetSupportedDevices()`. Without Linux NPU discovery, those EPs cannot claim NPU devices and provider selection policies such as `PREFER_NPU` silently fall back to CPU. Fixes #28660.
1 parent 0931c1a commit 5c7ba13

3 files changed

Lines changed: 261 additions & 2 deletions

File tree

onnxruntime/core/platform/linux/device_discovery.cc

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
#include "core/platform/device_discovery.h"
5+
#include "core/platform/linux/npu_device_discovery.h"
56
#include "core/platform/linux/pci_device_discovery.h"
67

78
#include <filesystem>
@@ -123,7 +124,7 @@ Status GetPciBusId(const std::filesystem::path& sysfs_path, std::optional<std::s
123124

124125
std::error_code error_code;
125126
auto pci_bus_id_path = std::filesystem::canonical(sysfs_path / "device", error_code); // resolves symlink to PCI bus id, e.g. 0000:65:00.0
126-
ORT_RETURN_IF_ERROR(ErrorCodeToStatus(error_code, sysfs_path / "device", "Getting PCI bus id from DRM device by resolving symlink"));
127+
ORT_RETURN_IF_ERROR(ErrorCodeToStatus(error_code, sysfs_path / "device", "Getting PCI bus id from sysfs device by resolving symlink"));
127128

128129
auto pci_bus_id_filename = pci_bus_id_path.filename();
129130
if (std::regex_match(pci_bus_id_filename.string(), pci_bus_id_regex)) {
@@ -269,6 +270,7 @@ Status GetGpuDeviceFromPci(const GpuPciPathInfo& path_info, OrtHardwareDevice& g
269270
namespace {
270271

271272
constexpr const char* kSysfsPciDevicesPath = "/sys/bus/pci/devices";
273+
constexpr const char* kSysfsAccelPath = "/sys/class/accel";
272274

273275
Status GetGpuDevices(std::vector<OrtHardwareDevice>& gpu_devices_out) {
274276
std::vector<GpuSysfsPathInfo> gpu_sysfs_path_infos{};
@@ -315,6 +317,119 @@ Status GetGpuDevices(std::vector<OrtHardwareDevice>& gpu_devices_out) {
315317

316318
} // namespace
317319

320+
namespace npu_device_discovery {
321+
322+
Status DetectNpuSysfsPaths(const fs::path& sysfs_accel_path,
323+
std::vector<NpuSysfsPathInfo>& npu_sysfs_paths_out) {
324+
std::error_code error_code{};
325+
326+
const bool sysfs_accel_path_exists = fs::exists(sysfs_accel_path, error_code);
327+
ORT_RETURN_IF_ERROR(ErrorCodeToStatus(error_code, sysfs_accel_path, "Checking existence of accel sysfs path"));
328+
329+
if (!sysfs_accel_path_exists) {
330+
npu_sysfs_paths_out = {};
331+
return Status::OK();
332+
}
333+
334+
const auto detect_accel_path = [](const fs::path& sysfs_path, size_t& accel_idx) -> bool {
335+
const auto filename = sysfs_path.filename();
336+
const auto filename_str = std::string_view{filename.native()};
337+
338+
// Look for a filename matching "accelN". N is a number.
339+
constexpr std::string_view prefix = "accel";
340+
if (filename_str.find(prefix) != 0) {
341+
return false;
342+
}
343+
344+
size_t parsed_accel_idx{};
345+
if (!TryParseStringWithClassicLocale<size_t>(filename_str.substr(prefix.size()), parsed_accel_idx)) {
346+
return false;
347+
}
348+
349+
accel_idx = parsed_accel_idx;
350+
return true;
351+
};
352+
353+
std::vector<NpuSysfsPathInfo> npu_sysfs_paths{};
354+
355+
auto dir_iterator = fs::directory_iterator{sysfs_accel_path, error_code};
356+
ORT_RETURN_IF_ERROR(ErrorCodeToStatus(error_code, sysfs_accel_path, "Iterating over accel sysfs devices"));
357+
358+
for (const auto& dir_item : dir_iterator) {
359+
const auto& dir_item_path = dir_item.path();
360+
361+
if (size_t accel_idx{}; detect_accel_path(dir_item_path, accel_idx)) {
362+
NpuSysfsPathInfo path_info{};
363+
path_info.accel_idx = accel_idx;
364+
path_info.path = dir_item_path;
365+
npu_sysfs_paths.emplace_back(std::move(path_info));
366+
}
367+
}
368+
369+
npu_sysfs_paths_out = std::move(npu_sysfs_paths);
370+
return Status::OK();
371+
}
372+
373+
Status GetNpuDeviceFromSysfs(const NpuSysfsPathInfo& path_info,
374+
OrtHardwareDevice& npu_device_out) {
375+
OrtHardwareDevice npu_device{};
376+
377+
const auto& sysfs_path = path_info.path;
378+
379+
uint16_t vendor_id{};
380+
const auto vendor_id_path = sysfs_path / "device" / "vendor";
381+
ORT_RETURN_IF_ERROR(ReadValueFromFile(vendor_id_path, vendor_id));
382+
npu_device.vendor_id = vendor_id;
383+
384+
uint16_t device_id{};
385+
const auto device_id_path = sysfs_path / "device" / "device";
386+
ORT_RETURN_IF_ERROR(ReadValueFromFile(device_id_path, device_id));
387+
npu_device.device_id = device_id;
388+
389+
npu_device.metadata.Add("accel_idx", MakeString(path_info.accel_idx));
390+
391+
std::optional<std::string> pci_bus_id;
392+
ORT_RETURN_IF_ERROR(GetPciBusId(sysfs_path, pci_bus_id));
393+
if (pci_bus_id) {
394+
npu_device.metadata.Add("pci_bus_id", std::move(*pci_bus_id));
395+
}
396+
397+
npu_device.type = OrtHardwareDeviceType_NPU;
398+
399+
npu_device_out = std::move(npu_device);
400+
401+
return Status::OK();
402+
}
403+
404+
} // namespace npu_device_discovery
405+
406+
namespace {
407+
408+
Status GetNpuDevices(std::vector<OrtHardwareDevice>& npu_devices_out) {
409+
std::vector<npu_device_discovery::NpuSysfsPathInfo> npu_sysfs_path_infos{};
410+
ORT_RETURN_IF_ERROR(npu_device_discovery::DetectNpuSysfsPaths(kSysfsAccelPath, npu_sysfs_path_infos));
411+
412+
std::vector<OrtHardwareDevice> npu_devices{};
413+
npu_devices.reserve(npu_sysfs_path_infos.size());
414+
415+
for (const auto& npu_sysfs_path_info : npu_sysfs_path_infos) {
416+
OrtHardwareDevice npu_device{};
417+
if (auto status = npu_device_discovery::GetNpuDeviceFromSysfs(npu_sysfs_path_info, npu_device); !status.IsOK()) {
418+
LOGS_DEFAULT(WARNING) << MakeString("Failed to detect devices under ",
419+
npu_sysfs_path_info.path,
420+
": ",
421+
status.ErrorMessage());
422+
continue;
423+
}
424+
npu_devices.emplace_back(std::move(npu_device));
425+
}
426+
427+
npu_devices_out = std::move(npu_devices);
428+
429+
return Status::OK();
430+
}
431+
} // namespace
432+
318433
std::unordered_set<OrtHardwareDevice> DeviceDiscovery::DiscoverDevicesForPlatform() {
319434
std::unordered_set<OrtHardwareDevice> devices;
320435

@@ -334,7 +449,16 @@ std::unordered_set<OrtHardwareDevice> DeviceDiscovery::DiscoverDevicesForPlatfor
334449
}
335450

336451
// get NPU devices
337-
// TODO figure out how to discover these
452+
{
453+
std::vector<OrtHardwareDevice> npu_devices{};
454+
Status npu_device_discovery_status = GetNpuDevices(npu_devices);
455+
if (npu_device_discovery_status.IsOK()) {
456+
devices.insert(std::make_move_iterator(npu_devices.begin()),
457+
std::make_move_iterator(npu_devices.end()));
458+
} else {
459+
LOGS_DEFAULT(WARNING) << "NPU device discovery failed: " << npu_device_discovery_status.ErrorMessage();
460+
}
461+
}
338462

339463
return devices;
340464
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
// This header exposes Linux NPU device discovery internals for testing.
5+
6+
#pragma once
7+
8+
#include <cstddef>
9+
#include <filesystem>
10+
#include <vector>
11+
12+
#include "core/common/status.h"
13+
#include "core/session/abi_devices.h"
14+
15+
namespace onnxruntime {
16+
namespace npu_device_discovery {
17+
18+
struct NpuSysfsPathInfo {
19+
size_t accel_idx;
20+
std::filesystem::path path;
21+
};
22+
23+
// Scans the given sysfs accel directory for NPU accel devices.
24+
Status DetectNpuSysfsPaths(const std::filesystem::path& sysfs_accel_path,
25+
std::vector<NpuSysfsPathInfo>& npu_sysfs_paths_out);
26+
27+
// Reads vendor/device IDs and populates an OrtHardwareDevice from an accel sysfs path.
28+
Status GetNpuDeviceFromSysfs(const NpuSysfsPathInfo& path_info,
29+
OrtHardwareDevice& npu_device_out);
30+
31+
} // namespace npu_device_discovery
32+
} // namespace onnxruntime
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
#include "core/platform/linux/npu_device_discovery.h"
5+
6+
#include <algorithm>
7+
#include <filesystem>
8+
#include <fstream>
9+
#include <string>
10+
#include <vector>
11+
12+
#include "gtest/gtest.h"
13+
#include "test/util/include/asserts.h"
14+
15+
namespace fs = std::filesystem;
16+
17+
namespace onnxruntime::test {
18+
namespace {
19+
20+
void WriteFile(const fs::path& path, const std::string& value) {
21+
std::ofstream f(path);
22+
f << value;
23+
}
24+
25+
class NpuDeviceDiscoveryTest : public ::testing::Test {
26+
protected:
27+
void SetUp() override {
28+
temp_dir_ = fs::temp_directory_path() / "ort_npu_discovery_test";
29+
fs::remove_all(temp_dir_);
30+
fs::create_directories(temp_dir_);
31+
}
32+
33+
void TearDown() override {
34+
fs::remove_all(temp_dir_);
35+
}
36+
37+
fs::path temp_dir_;
38+
};
39+
40+
} // namespace
41+
42+
TEST_F(NpuDeviceDiscoveryTest, ReturnsEmptyForNonexistentPath) {
43+
std::vector<npu_device_discovery::NpuSysfsPathInfo> npu_paths;
44+
ASSERT_STATUS_OK(npu_device_discovery::DetectNpuSysfsPaths(temp_dir_ / "nonexistent", npu_paths));
45+
EXPECT_TRUE(npu_paths.empty());
46+
}
47+
48+
TEST_F(NpuDeviceDiscoveryTest, DetectsAccelDevices) {
49+
fs::create_directories(temp_dir_ / "accel0");
50+
fs::create_directories(temp_dir_ / "accel12");
51+
fs::create_directories(temp_dir_ / "renderD128");
52+
fs::create_directories(temp_dir_ / "accelabc");
53+
54+
std::vector<npu_device_discovery::NpuSysfsPathInfo> npu_paths;
55+
ASSERT_STATUS_OK(npu_device_discovery::DetectNpuSysfsPaths(temp_dir_, npu_paths));
56+
57+
ASSERT_EQ(npu_paths.size(), 2u);
58+
59+
std::vector<size_t> accel_indices;
60+
accel_indices.reserve(npu_paths.size());
61+
for (const auto& npu_path : npu_paths) {
62+
accel_indices.push_back(npu_path.accel_idx);
63+
}
64+
65+
std::sort(accel_indices.begin(), accel_indices.end());
66+
67+
EXPECT_EQ(accel_indices[0], 0u);
68+
EXPECT_EQ(accel_indices[1], 12u);
69+
}
70+
71+
TEST_F(NpuDeviceDiscoveryTest, GetNpuDeviceFromSysfsReadsVendorDeviceAndMetadata) {
72+
const auto pci_device_dir = temp_dir_ / "pci_devices" / "0000:65:00.0";
73+
const auto accel_dir = temp_dir_ / "class_accel" / "accel0";
74+
75+
fs::create_directories(pci_device_dir);
76+
fs::create_directories(accel_dir);
77+
78+
WriteFile(pci_device_dir / "vendor", "0x1022");
79+
WriteFile(pci_device_dir / "device", "0x1502");
80+
81+
std::error_code error_code{};
82+
fs::create_directory_symlink(pci_device_dir, accel_dir / "device", error_code);
83+
ASSERT_FALSE(error_code) << error_code.message();
84+
85+
npu_device_discovery::NpuSysfsPathInfo path_info{};
86+
path_info.accel_idx = 0;
87+
path_info.path = accel_dir;
88+
89+
OrtHardwareDevice npu_device{};
90+
ASSERT_STATUS_OK(npu_device_discovery::GetNpuDeviceFromSysfs(path_info, npu_device));
91+
92+
EXPECT_EQ(npu_device.type, OrtHardwareDeviceType_NPU);
93+
EXPECT_EQ(npu_device.vendor_id, 0x1022u);
94+
EXPECT_EQ(npu_device.device_id, 0x1502u);
95+
96+
const auto& entries = npu_device.metadata.Entries();
97+
EXPECT_NE(entries.find("accel_idx"), entries.end());
98+
EXPECT_EQ(entries.at("accel_idx"), "0");
99+
EXPECT_NE(entries.find("pci_bus_id"), entries.end());
100+
EXPECT_EQ(entries.at("pci_bus_id"), "0000:65:00.0");
101+
}
102+
103+
} // namespace onnxruntime::test

0 commit comments

Comments
 (0)