Skip to content

Add Intel Gaudi HPU support through libhlml - #528

Merged
Syllo merged 17 commits into
Syllo:masterfrom
dot1x:gaudi-support
Nov 15, 2026
Merged

Syllo merged 17 commits into
Syllo:masterfrom
dot1x:gaudi-support

Conversation

@dot1x

@dot1x dot1x commented Oct 10, 2026 •

Copy link
Copy Markdown

This adds a backend for Intel Gaudi (Habana Labs) HPUs. It is built on Linux by default through the new GAUDI_SUPPORT option.

The backend loads libhlml.so at runtime. HLML is the management library behind hl-smi and is installed with the habanalabs driver packages. Because the library is loaded at runtime, building nvtop does not need the Habana SDK headers. On machines without the habanalabs module, the backend bails out before calling dlopen.

 Device 0 [Gaudi2 HL-225] PCIe GEN 4@16x RX: 0.000 KiB/s TX: 6.000 KiB/s Device 1 [Gaudi2 HL-225] PCIe GEN 4@16x RX: 0.000 KiB/s TX: 0.000 KiB/s
 HPU 1650MHz HBM N/A MHz  TEMP  37°C   FAN N/A   POW  76 / 550 W         HPU 1650MHz HBM N/A MHz  TEMP  37°C   FAN N/A   POW  68 / 550 W
 HPU[                    0%(eff 0%)] MEM[|||||||||||||87.980Gi/96.000Gi] HPU[                    0%(eff 0%)] MEM[|||||||||||||87.980Gi/96.000Gi]
...
    PID USER DEV     TYPE  GPU        GPU MEM    CPU  HOST MEM Command
 399951 test   3  Compute   0%  89371MiB  91%   100%   3801MiB VLLM::Worker_TP0_EP0
 400320 test   1  Compute   0%  89324MiB  91%   100%   3813MiB VLLM::Worker_TP2_EP2

What is reported

  • Static:
    • device name (Gaudi2 HL-225)
    • architecture
    • memory type (HBM, or DDR4 for Goya)
    • max PCIe link
    • shutdown/slowdown temperatures
  • Dynamic:
    • AIP utilization
    • device memory
    • SoC clock and its max
    • chip temperature
    • power draw and limit
    • current PCIe link (from sysfs, since HLML returns "not supported" on Gaudi2)
    • PCIe rx/tx throughput
  • Processes:
    • each process using a device
    • its device memory
    • its utilization

Gaudi devices are labeled HPU (gpu_processing_unit_hpu), like the existing PPU label for T-Head.

Design notes

  • Background sampling. Several HLML calls go through the device firmware. hlml_device_get_pcie_throughput takes about 25 ms per call and per direction, and the utilization and power calls take 3 to 6 ms each. On an 8-card machine, querying in the UI loop added more than 0.5 s per refresh.
    • The dynamic info is therefore sampled by a background thread, and refresh_dynamic_info only copies the latest sample.
    • Sampling is driven by one-shot refresh requests, so the thread is idle when nvtop is not refreshing. A request made while a device is already being sampled schedules another round instead of being lost.
    • The first refresh samples synchronously, so the first frame and -s already have data. After that, only the sampler thread calls HLML, so the backend does not rely on libhlml being thread safe. If the thread cannot be created, the backend falls back to synchronous sampling.
  • Temperature. hlml_device_get_temperature leaks memory on every call: about 260 bytes, from sensors_get_label inside libhlml, found with ASan. With 8 cards refreshing every second, that is about 180 MB a day.
    • HLML computes the chip temperature as the hottest hwmon channel mapped by its hardcoded per-ASIC ON_AIP table. The driver's /etc/sensors.d/hl_sensors.conf labels those channels On Chip ... and covers Gaudi 1, 2 and 3.
    • The backend parses that file once for the board's chip block (honoring ignore), then reads those hwmon channels directly. The number of selected channels is checked against HLML's per-ASIC map (Gaudi2: 8, Gaudi3 OAM/FPGA: 16, Gaudi3 HL-338: 12) so a partial or user-modified config falls back to HLML instead of reporting the maximum of the wrong set.
    • HLML is only used as a fallback when the config is missing or does not match.
  • Processes. HLML cannot enumerate processes, so the backend does the same as hl-smi: it scans /proc/*/fd for the /dev/accel/accelN compute node.
    • Nodes are matched by st_rdev rather than by path, so devices remapped inside containers are attributed correctly.
    • The control node accel_controlD*, which monitoring tools open, is ignored.
    • /proc is scanned once per refresh, not once per device.
    • Gaudi allows a single compute context per device. The process memory is the value the framework publishes in /dev/shm/mem_usage_accelN. The process keeps that file open, so it is read through /proc/<pid>/fd, which also works across mount namespaces. If the framework does not publish a value, the process memory is N/A; the device-wide used memory is not attributed to the process because it includes the 768 MiB the device reserves.
    • The process utilization is the device utilization when a single process can be identified as the compute context: the only process holding the compute node, or the single process publishing the framework's memory when several processes inherited it after a fork.
  • libhlml ABI. The output structures are transcribed from the 1.24 SDK because nvtop must build without the Habana headers. libhlml writes the size of its own structure into the caller's object, with no size negotiation, so the utilization and memory output structures have a 512-byte reserved tail. This absorbs ABI growth from a newer library instead of overwriting the stack. The transcribed prefixes are checked with compile-time offset and size assertions.

Shared code

Besides the CMake option, the README section and the new test, the shared code gains the gpu_processing_unit_hpu enum value (following the PPU precedent) and its name mapping. Device enumeration also rejects a second call instead of indexing the previous device array, matching the PPU backend.

Testing

Tested on 8x Gaudi2 HL-225, habanalabs driver 1.24.1, Ubuntu 24.04 (kernel 6.17), while vLLM was serving on all cards:

  • Compared with hl-smi at the same time, nvtop shows the same per-process memory to the MiB, the same device memory, and the same power and temperature within the noise between two reads.
  • The -s / -l snapshots include device_architecture: Gaudi2 and memory_type: HBM.
  • About 550 refreshes under ThreadSanitizer (nvtop -l -d 1 for 60 s): no reports.
  • About 550 refreshes under AddressSanitizer (same run): no errors. The only leak report is a fixed 26.8 KB allocated by hlml_init/hlml_shutdown inside libhlml, and it is the same after 15 s and after 60 s.
  • nvtop uses about 2% CPU in total (main thread plus sampler) with the default 1 s refresh.
  • No new warnings with -Wall -Wextra. The new files are formatted with the repository's .clang-format.

The new gaudiTests target runs in CI and covers the libhlml ABI growth padding, double enumeration, device-name composition, AIP sensor count validation, lm-sensors config parsing, PCI chip address conversion, /proc scanning with a fake tree (including container remapping), process memory/utilization attribution, and the sampler request helpers. It passes under AddressSanitizer/UBSan and ThreadSanitizer.

Known differences with hl-smi

  • The power limit is the HLML enforced limit of the main 54 V supply, 550 W on HL-225. hl-smi's summary adds the 50 W of the auxiliary 12 V supply, which HLML does not expose.
  • Goya is enumerated by HLML and reported with DDR4 memory, but it has not been validated on hardware.

Tony and others added 17 commits October 10, 2026 14:39
New backend (GAUDI_SUPPORT, enabled by default on Linux) that loads
libhlml.so, the library behind hl-smi, at runtime, so the Habana SDK
headers are not needed to build.

- Reports AIP utilization, device memory, SoC clock, chip temperature,
  power draw and limit, PCIe link (from sysfs) and rx/tx throughput,
  device architecture (Gaudi2) and memory type (HBM).
- Some HLML queries go through the firmware (PCIe throughput takes
  ~25ms per call and direction), so the dynamic info is sampled by a
  background thread. After the first synchronous sample only that thread
  calls HLML, so libhlml thread safety is not assumed.
- hlml_device_get_temperature leaks memory on every call
  (sensors_get_label inside libhlml). The chip temperature is read from
  the same hwmon channels instead: the "On Chip" sensors listed for the
  board in /etc/sensors.d/hl_sensors.conf, as HLML does.
- HLML does not list processes. Like hl-smi, the backend scans /proc for
  the /dev/accel/accelN node (matched by st_rdev, so remapped device
  nodes in containers work). Gaudi runs one compute context per device.
  The process memory is the value the framework publishes in
  /dev/shm/mem_usage_accelN, read through the process's own fd, which is
  what hl-smi reports.

Also merge the two copies of the supported-vendor sentence in the
README.

Tested on 8x Gaudi2 HL-225 with driver 1.24.1: values match hl-smi.
Clean under ThreadSanitizer and AddressSanitizer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	CMakeLists.txt
#	README.md
gpuinfo_gaudi_get_device_handles indexed gaudi_devices with the
gaudi_device_count left over from a previous enumeration. Calling it
twice without an intervening shutdown wrote past the newly allocated
array (heap-buffer-overflow under AddressSanitizer) and leaked the
previous allocation.

Reject the second call like the PPU backend does, and add a gaudiTests
test which enumerates twice and then checks that enumeration works again
after a shutdown.
The HLML structures are hand-transcribed from the 1.24 SDK because nvtop
must build without the Habana headers. Spell out that the output
structures are an ABI contract, mark the fields that only exist for the
layout, and add compile-time size and offset assertions so that an
accidental edit (or a bad merge resolution) is caught at build time.

The assertions document the target ABI; they cannot detect a newer
libhlml that has grown one of the structures, so the comment asks for a
recheck when the SDK bumps its API and for the transcribed block to stay
in sync with it.
nvtop transcribes the HLML structures because it must build without the
Habana SDK headers. The API has no size negotiation and libhlml writes
the size of its own structure into the caller's object, so a newer
library that grew a structure would overflow nvtop's stack. The
compile-time assertions only protect nvtop's own definitions.

Give the utilization and memory output structures a 512-byte reserved
tail, assert the transcribed 1.24 prefix offsets, and keep the full-size
assertions. Extend gaudiTests with fake entry points that write 256 bytes
past the known prefix, emulating a newer library, and verify the values
still arrive intact under AddressSanitizer and UBSan.
When the framework did not publish /dev/shm/mem_usage_accelN, a single
process on the device was attributed the whole device used memory. That
value includes the 768 MiB the device reserves and any non-framework
allocations, so it did not match what hl-smi shows and overstated the
process usage.

Leave the process memory unset (N/A) in that case instead. Add a test
that keeps a valid device used_memory present and checks that the
per-process field stays invalid, plus the existing shm value path.
Update the README wording accordingly.
Gaudi has a single compute context per device, but several processes can
hold the compute node after a fork. Requiring exactly one holder hid the
per-process utilization in that case.

Attribute the device utilization to:
- the only process holding the device, or
- the only process publishing the framework's shm memory when several
  processes hold it.

Leave it unset when neither rule identifies a single process. Add tests
for the four cases and update the README.
HLML does not classify the AIP temperature sensors by their libsensors
labels: it uses a hardcoded per-ASIC temp_type map and calls
sensors_get_label() only for display, which is where the leak comes
from. nvtop uses the vendor config labels as a proxy for the same
channels; document that, and note that only the vendor file is read
because HLML ignores user relabels in other sensors.d files.

Also derive the libsensors PCI chip name from the real BDF instead of
the hardcoded "-pci-0", so exact "chip" statements match. Add a unit
test for the PCI address conversion and update the README.
populate_static_info() calls HLML from the main thread. It is safe only
because nvtop populates static info before the first dynamic refresh,
which starts the sampler and becomes the only HLML caller. Spell that out
in the file header and next to the function so a future runtime
re-population path does not silently call HLML concurrently with the
sampler.
The per-device monitored flag was set on every refresh but never
cleared, so a device the user stopped monitoring kept being sampled
whenever another device triggered a round.

Make it a one-shot refresh request: the sampler clears it before
sampling the device, and recomputes sample_pending from the remaining
requests at the end of the round. Requests that arrive while sampling
are either served in the same round if their device has not been
visited yet, or trigger another round.
Lower-casing the whole sysfs device_type produced names such as
"Gaudi2b" and "Gaudi3 hl-338", and the HLML board name was appended even
when the device_type already contained it, producing
"Gaudi3 hl-338 HL-338".

Map the known ASIC strings to their display names, and only append the
board name when the family does not already end with it (ignoring case
and separators). Also report Goya, which HLML can enumerate, with DDR4
instead of HBM.

Add tests for the mapping, suffix comparison and name composition.
Add gpu_processing_unit_hpu to the shared enum and use it for the Gaudi
vendor. The per-device labels (utilization meter, clock, PCIe link, setup
window and plot legends) now show HPU instead of GPU. The process table keeps
its generic GPU/GPU MEM column names because it can mix vendors. The enum
already had PPU as a precedent.

Update the ProcessingUnitNames test.
HLML selects the AIP temperature sensors with a hardcoded per-ASIC map,
while nvtop identifies them through the vendor config labels. A partial
or user-modified config could select only some of them and report the
maximum of the wrong set.

Compare the selected count with HLML's map (Gaudi2: 8, Gaudi3 OAM/FPGA:
16, Gaudi3 HL-338: 12) and fall back to the HLML temperature call when
they differ. Unknown ASICs keep trusting the labels.

Add tests for the expected counts and the matching helper, and update
the README fallback description.
- Drop hlml_device_get_pci_info() and the PCI ABI structures. pdev now
  comes from the driver's pci_addr sysfs attribute, with a gaudi-<index>
  fallback. This removes the largest hand-transcribed structure; pci_addr
  is the same Linux BDF HLML was reading.
- Clear the HLML function pointers on shutdown and on partial init
  failure, copy dlerror() into a stable buffer, force the board name to
  be NUL-terminated, and drop the unused has_sample flag.
- Parse path_index() from basenames, tolerate a " (deleted)" suffix, and
  also match shm files by the device's host minor for containers that
  remap the accel node.
- Accept only shared-memory layout version 1 (the known one) and remove
  the arbitrary 4096-byte upper size bound.
- Clamp mem_util_rate to 100 and use hex for the pdev fallback so the
  format-truncation warning disappears.
- Name PyTorch/Paddle as the shm publishers in the code comment and
  README.

The pci_info zero-initialization nit went away with the structure. Goya
stays under the Gaudi backend name; the README documents it separately.
- Parse the lm-sensors configuration through a testable helper and check
  labels, ignore and the AIP channel count match.
- Cover path_index() basenames, "(deleted)" suffixes and control nodes.
- Exercise gaudi_scan_processes() with a fake proc tree: index match,
  the single-device/single-shm fallback, no match, and the host-minor
  fallback for a remapped accel node.
- Test the sampler request helpers: taking a request clears it, other
  pending requests remain for the next round.
Avoid walking past the string when sysfs reports an empty device_type, and
zero the HLML output structures so a library that writes an older, shorter
structure cannot leave the fields we read indeterminate. Cover the empty
device_type path in gaudiTests.

Fix the README tense and note that Goya has not been validated on hardware,
and mention the per-process utilization in the feature list.
@Syllo
Syllo merged commit 6334c7a into Syllo:master Nov 15, 2026
4 checks passed
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