feat(agent): monitor Intel Arc (xe) GPUs via nvtop (#2223)

intel_gpu_top does not support the xe driver, so skip it for xe devices
and let the existing nvtop last-resort collector handle them. nvtop leaves
device_name unset on xe, so name the GPU from its PCI device id ("Intel GPU
(<id>)"). Adds nvtop to the Intel agent image (gputop already ships with
igt-gpu-tools).
This commit is contained in:
Miłosz Kolber
2026-08-16 17:41:28 +02:00
committed by GitHub
parent 3688b2d033
commit bfa6a1e361
3 changed files with 47 additions and 4 deletions

View File

@@ -126,6 +126,7 @@ type gpuCapabilities struct {
hasAmdSysfs bool
hasTegrastats bool
hasIntelGpuTop bool
hasXe bool
hasIntelSysfs bool
hasNvtop bool
hasMacmon bool
@@ -449,7 +450,8 @@ func (gm *GPUManager) storeSnapshot(id string, gpu *system.GPUData, cacheKey uin
// It only reports capability presence and does not apply policy decisions.
func (gm *GPUManager) discoverGpuCapabilities() gpuCapabilities {
caps := gpuCapabilities{
hasAmdSysfs: gm.hasAmdSysfs(),
hasAmdSysfs: gm.hasAmdSysfs(),
hasXe: gm.hasXe(),
hasIntelSysfs: gm.hasIntelSysfs(),
}
if _, err := exec.LookPath(nvidiaSmiCmd); err == nil {
@@ -719,7 +721,7 @@ func (gm *GPUManager) resolveLegacyCollectorPriority(caps gpuCapabilities) []col
priorities = append(priorities, collectorSourceAmdSysfs)
}
if caps.hasIntelGpuTop {
if caps.hasIntelGpuTop && !caps.hasXe {
priorities = append(priorities, collectorSourceIntelGpuTop)
}
if caps.hasIntelSysfs {

View File

@@ -5,6 +5,7 @@ import (
"io"
"log/slog"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
@@ -48,9 +49,14 @@ func (gm *GPUManager) updateNvtopSnapshots(snapshots []nvtopSnapshot) bool {
valid := false
usedIDs := make(map[string]struct{}, len(snapshots))
var xeName string
for i, sample := range snapshots {
// nvtop leaves device_name unset on xe devices.
if sample.DeviceName == "" {
continue
if xeName == "" {
xeName = xeGpuName()
}
sample.DeviceName = xeName
}
indexID := "n" + strconv.Itoa(i)
id := indexID
@@ -158,3 +164,38 @@ func (gm *GPUManager) startNvtopCollector(interval string, onFailure func()) {
}
}()
}
// xeDevicePath returns the sysfs device path of the first xe GPU, or "".
func xeDevicePath() string {
cards, err := filepath.Glob("/sys/class/drm/card*")
if err != nil {
return ""
}
for _, card := range cards {
if strings.Contains(filepath.Base(card), "-") {
continue
}
if uevent, err := utils.ReadStringFileLimited(filepath.Join(card, "device", "uevent"), 4096); err == nil && strings.Contains(uevent, "DRIVER=xe") {
return filepath.Join(card, "device")
}
}
return ""
}
func (gm *GPUManager) hasXe() bool {
return xeDevicePath() != ""
}
// xeGpuName names an xe GPU from its PCI device id; nvtop leaves device_name unset on xe.
func xeGpuName() string {
devicePath := xeDevicePath()
if devicePath == "" {
return "GPU"
}
id, err := utils.ReadStringFileLimited(filepath.Join(devicePath, "device"), 64)
if err != nil {
return "GPU"
}
id = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(id, "0x")))
return "Intel GPU (" + id + ")"
}

View File

@@ -20,7 +20,7 @@ FROM alpine:3.23
COPY --from=builder /agent /agent
RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/testing igt-gpu-tools smartmontools
RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/testing igt-gpu-tools nvtop smartmontools
# Ensure data persistence across container recreations
VOLUME ["/var/lib/beszel-agent"]