From bfa6a1e361278ad50d69ccf073e558fb92680b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Kolber?= <143708325+miloszkolber@users.noreply.github.com> Date: Sun, 16 Aug 2026 17:41:28 +0200 Subject: [PATCH] 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 ()"). Adds nvtop to the Intel agent image (gputop already ships with igt-gpu-tools). --- agent/gpu.go | 6 +++-- agent/gpu_nvtop.go | 43 ++++++++++++++++++++++++++++++++- internal/dockerfile_agent_intel | 2 +- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/agent/gpu.go b/agent/gpu.go index 9b277f7b..b5ace45a 100644 --- a/agent/gpu.go +++ b/agent/gpu.go @@ -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 { diff --git a/agent/gpu_nvtop.go b/agent/gpu_nvtop.go index ce32f008..28136e8c 100644 --- a/agent/gpu_nvtop.go +++ b/agent/gpu_nvtop.go @@ -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 + ")" +} diff --git a/internal/dockerfile_agent_intel b/internal/dockerfile_agent_intel index a6166cca..030f0b47 100644 --- a/internal/dockerfile_agent_intel +++ b/internal/dockerfile_agent_intel @@ -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"]