Skip to content

fan: multi-fan RPM with sysfs hwmon fallback for Battlemage/xe (Arc P… - #148

Open
thevisad wants to merge 1 commit into
intel:mainfrom
thevisad:xpum-fan-rpm
Open

fan: multi-fan RPM with sysfs hwmon fallback for Battlemage/xe (Arc P…#148
thevisad wants to merge 1 commit into
intel:mainfrom
thevisad:xpum-fan-rpm

Conversation

@thevisad

@thevisad thevisad commented Aug 2, 2026

Copy link
Copy Markdown

Description

xpu-smi stats reports Fan Speed (RPM): N/A on the Intel Arc Pro B70 (Battlemage, xe driver) even
though the fan is spinning and its RPM is published on the PCI device's hwmon node (fanN_input): Level
Zero sysman enumerates no fan handle on this device. This PR adds per-fan RPM reporting with a sysfs-hwmon
fallback for that case, a correct Level Zero error contract, genuine per-fan identity, and validated
discovery, while leaving the existing percent path byte-identical where Level Zero exposes fan handles.

What it does

  • RPM source is chosen by a pure, unit-tested helper decideFanRpmSource(enumResult, fanCount):
    enumeration failure → propagate; fanCount == 0 → sysfs; fanCount >= 1 → Level Zero. sysfs is
    never consulted when a fan handle exists.
  • Genuine multi-fan: getSpeedRpmById reads a specific L0 handle (propagates its error; accepts
    rpm >= 0); getAllSpeedsRpm iterates all L0 handles keyed by real fan id, and in the zero-handle
    case enumerates all fanN_input nodes (fan1_input→0, fan2_input→1, …). The stats collector
    keys samples by real fan id — no fan is dropped.
  • Zero RPM (a stopped fan) is a valid recorded reading, distinct from a failed read.
  • resolveSysfsHwmon sets zes_pci_properties_t.stype (required by the Level Zero API contract), scans
    all hwmon* nodes, and accepts a candidate only when a real RPM value parses (not merely that the
    file opens), so a malformed node cannot shadow a valid one.
  • readFanInputFromDir returns ZE_RESULT_ERROR_INVALID_NULL_POINTER for a null out-pointer;
    parseFanInputIndex uses std::from_chars and rejects overflow.
  • Pure helpers live in an internal xpum::hwmon utility (hwmon_fan_utils.{h,cpp}), not on the exported
    fan class. The HAL getters propagate Level Zero errors and never substitute a sysfs value for a real
    driver failure; the stats collector treats an unavailable RPM sample as absent (like other optional
    xpu-smi stats metrics), so it does not make xpu-smi exit with the Level Zero failure.

Testing

  • doctest: 18 cases / 83 assertions (decideFanRpmSource matrix incl. inconsistent
    fanCount>0 && handles==nullptr→internal error; RPM parse incl. 0, malformed, overflow; index parse
    • overflow boundary; readFanInputFromDir against a temp hwmon dir incl. null-ptr classification;
      percent-rendering unchanged vs " RPM" rendering). Full meson test: 11/11 suites.
  • Hardware (Arc Pro B70, xe, kernel 7.0.0-28-generic, Level Zero 1.27.0): xpu-smi stats reports
    Fan Speed (RPM) Fan 0: 1177 RPM, tracking raw fan1_input; Fan Speed (%) stays N/A; discovery
    selected the correct hwmon node.

Scope / limitations

  • Multi-fan is genuinely implemented (all L0 handles by id; all fanN_input in the fallback) but
    hardware-validated only on the single-fan B70; the multi-fan paths are exercised by unit tests, not on
    multi-fan silicon.
  • The Level-Zero-handle state-read-error branch (zesFanGetState failing on a present handle) is
    covered by the selection logic but not unit-mocked, because injecting that failure would require
    fabricating a device handle; documented in the code.
  • Driver note: the B70 fan tachometer returns EAGAIN on a back-to-back read, so hwmon resolution is
    validated once and cached rather than re-read every sample.
  • No unguarded POSIX-only header remains (std::filesystem). Windows was not build-tested. The doctest
    test targets require a doctest package in CI.

Fixes:
Relates-To:

Type of change

  • Bug fix
  • New feature
  • API / ABI change
  • Refactor / cleanup
  • Documentation
  • Tests
  • Dependency update
  • CI / build
  • Other

Affected components

  • hal — Hardware Abstraction Layer
  • ial — Interface / Application Layer
  • oal — OS Abstraction Layer
  • xpumd — XPU Manager Daemon
  • cli / smi
  • ci / build system

Checklist

  • Commit messages follow the project guidelines
  • Each commit includes Signed-off-by (DCO)
  • New public APIs or CLI options are documented — the new Fan Speed (RPM) row / fan.speed_rpm
    metric is described here; no new CLI option
  • No unrelated changes included
  • Squash "fixup" commits after reviews — n/a (single commit)

@savery42

savery42 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Related to #146

@savery42

savery42 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Hi @thevisad thanks for the contributions!

No issues with Windows build or runtime for this PR as well as #149 on Windows Server {2022,2025} and w/ Visual Studio {17(2022),18(2026)}. I really need to set up build actions... :).

I'll take a proper look today and should be able to grab a multi-tile system for verification there as well.

@thevisad

thevisad commented Aug 3, 2026

Copy link
Copy Markdown
Author

@savery42 thanks! One small heads-up from a self-check on my end, since it slipped past both the Linux build and your Windows build:

I compile-checked the two internal util TUs standalone under MSVC with /WX, and it surfaced a C4273 (inconsistent dll linkage) on this PR — the internal xpum::hwmon fan helpers in hwmon_fan_utils.h were annotated LIBXPUM_API, i.e. exported from the DLL. That's inconsistent with the temperature helpers (which are plain) and with the goal of keeping them off the exported surface. The full DLL build resolves the macro consistently so it stays green, which is why your Windows build was clean, and it's benign on Linux, so it hid on both sides; only a standalone /WX compile flagged it.

Fixed: dropped LIBXPUM_API from the five internal decls. meson test is still 11/11, and both util TUs now compile clean under MSVC /std:c++20 /WX (VS 2022). Pushed to the branch, head 4c089ee. #149 (temperature) was already clean — its helpers never carried the macro.

Appreciate you grabbing a multi-tile box for verification — thanks for taking the time on these.

@savery42 savery42 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same general comment on the temp PR regrding OS specific code to be placed in the OAL

Comment thread ial/cmn/cmd_stats.cpp Outdated
Comment on lines +1752 to +1755
std::ostringstream oss;
oss << std::fixed << std::setprecision(precision);
oss << "Fan " << fanId << ": " << value << unit;
return oss.str();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// c++20 style / nit

Suggested change
std::ostringstream oss;
oss << std::fixed << std::setprecision(precision);
oss << "Fan " << fanId << ": " << value << unit;
return oss.str();
return std::format("Fan {}: {:.{}f}{}", fanId, value, precision, unit);

Comment thread hal/core/hwmon_fan_utils.cpp Outdated
Comment on lines +62 to +63
static const std::string prefix = "fan";
static const std::string suffix = "_input";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// c++20 / nit

Suggested change
static const std::string prefix = "fan";
static const std::string suffix = "_input";
constexpr std::string_view prefix = "fan";
constexpr std::string_view suffix = "_input";

Avoids allocation

Comment thread hal/core/hwmon_fan_utils.cpp Outdated
Comment on lines +64 to +69
if (name.size() <= prefix.size() + suffix.size()) {
return false;
}
if (name.compare(0, prefix.size(), prefix) != 0) {
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// c++20 / nit

starts_with(), ends_with()

Comment thread hal/core/hwmon_fan_utils.cpp Outdated
Comment on lines +77 to +81
for (char c : digits) {
if (c < '0' || c > '9') {
return false;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (char c : digits) {
if (c < '0' || c > '9') {
return false;
}
}

std::from_chars will already stop at the first non-digit, so caught by res.ptr != end

Report fan RPM on the tested Arc Pro B70 when Level Zero Sysman enumerates no
fan handles. Move hwmon traversal into the OS abstraction layer, preserve Level
Zero as authoritative when a handle exists, retain valid 0-RPM readings, add the
RPM stats row, and cover discovery/parsing with Linux tests and Windows compile
validation.

Signed-off-by: thevisad <373694+thevisad@users.noreply.github.com>
@thevisad

Copy link
Copy Markdown
Author

@savery42 Thanks. Same OAL treatment here.

OAL move. The fan hwmon traversal is now in oal/lin/hwmon_fan.cpp: getHwmonInputPaths(bdf, prefix) returns {zero-based fan index -> fanN_input path} (strict fan_input matching, index-overflow rejected), with a Windows stub in oal/win/hwmon_fan.cpp. OAL only discovers the fanN_input paths; the RPM read/parse/validate and the Level-Zero-vs-sysfs decision stay in hal (hwmon_fan_utils). Per your point, I deliberately did not push RPM value interpretation down into the traversal; OAL stays pure filesystem discovery.

Contract. decideFanRpmSource: successful enumeration with >= 1 handle picks Level Zero; enumeration reporting no fan or denying an unprivileged process picks sysfs (the #146 normal-user case); 0 handles picks sysfs; any other enumeration failure propagates. Genuine per-fan identity: getAllSpeedsRpm iterates all Level Zero handles keyed by real fan id, and in the zero-handle case reads every fanN_input OAL resolved, so no fan is dropped. Per-fan row formatting moved to std::format, locked by a cmd_stats regression test.

Tests. New OAL suite oal/lin/test/hwmon_fan_tests (7 / 26), plus fan_tests (14 / 53) and cmd_stats_test (3 / 8). meson test: 12/12 suites green. Hardware: the B70 reports Fan Speed (RPM) Fan 0: 1177 RPM tracking fan1_input. Scope: the tested B70 exposes a single fan hwmon node; the multi-fan paths are unit-tested but not run on multi-fan silicon (called out in the PR body).

Windows. The touched surface (oal/win/hwmon_fan.cpp, hwmon_fan_utils.cpp) is verified clean with MSVC 19.44 at /std:c++20 /WX, and that pass actually caught a real defect (the fan internal helpers were LIBXPUM_API-exported; removed). This is a compile check of the modified TUs, not a full Windows product build.

Single DCO-signed commit; the PR description is updated to match. Relates to #146.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants