From 6141b15f0359516101e4da0fc4350f4d04f165aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Kolber?= <143708325+miloszkolber@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:59:52 +0200 Subject: [PATCH] fix(agent): SKIP_GPU excludes GPU hwmon from temperatures and fans (#2313) --- agent/agent.go | 6 +- agent/connection_manager.go | 1 + agent/fans.go | 20 ++++- agent/fans_test.go | 17 +++++ agent/gpu.go | 31 +++++++- agent/sensors.go | 125 ++++++++++++++++++++++++++++++- agent/sensors_test.go | 144 +++++++++++++++++++++++++++++++++++- 7 files changed, 336 insertions(+), 8 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 454c565e..998a32a7 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -252,7 +252,11 @@ func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedD // Start initializes and starts the agent with optional WebSocket connection func (a *Agent) Start(serverOptions ServerOptions) error { a.keys = serverOptions.Keys - return a.connectionManager.Start(serverOptions) + err := a.connectionManager.Start(serverOptions) + if err != nil { + a.cleanupSensorShadow() + } + return err } func (a *Agent) getFingerprint() string { diff --git a/agent/connection_manager.go b/agent/connection_manager.go index 0f29bd24..8152817e 100644 --- a/agent/connection_manager.go +++ b/agent/connection_manager.go @@ -155,6 +155,7 @@ func (c *ConnectionManager) stop() error { _ = c.agent.StopServer() c.agent.monitorManager.Stop() c.closeWebSocket() + c.agent.cleanupSensorShadow() return health.CleanUp() } diff --git a/agent/fans.go b/agent/fans.go index 2562d8d2..7b312a58 100644 --- a/agent/fans.go +++ b/agent/fans.go @@ -12,7 +12,7 @@ import ( ) type fanSensor struct { - key, path string + key, path, chip string } var getFanSensors = newFanSensorCache(hwmonRoot) @@ -34,6 +34,10 @@ func (a *Agent) updateFans(systemStats *system.Stats) { slog.Debug("Error reading fans", "err", err) return } + // Filter before reading fan*_input: each read can wake an idle GPU. + if a.sensorConfig != nil && a.sensorConfig.skipGPU { + sensors = filterGpuFans(sensors) + } fans := readFanSensors(sensors) if len(fans) == 0 { return @@ -100,7 +104,7 @@ func discoverHwmonFans(root string) ([]fanSensor, error) { if label != "" { key = chipName + "_" + label } - sensors = append(sensors, fanSensor{key, inputPath}) + sensors = append(sensors, fanSensor{key, inputPath, chipName}) } } return sensors, nil @@ -115,3 +119,15 @@ func readFanSensors(sensors []fanSensor) map[string]uint16 { } return fans } + +// filterGpuFans drops GPU chips without touching the shared cache backing array. +func filterGpuFans(sensors []fanSensor) []fanSensor { + kept := make([]fanSensor, 0, len(sensors)) + for _, sensor := range sensors { + if isGpuChipName(sensor.chip) { + continue + } + kept = append(kept, sensor) + } + return kept +} diff --git a/agent/fans_test.go b/agent/fans_test.go index 3bdb480d..113dc009 100644 --- a/agent/fans_test.go +++ b/agent/fans_test.go @@ -103,3 +103,20 @@ func TestFanDiscoveryCache(t *testing.T) { fans = readFanSensors(sensors) assert.Equal(t, map[string]uint16{"chip_fan1": 1200}, fans) } + +func TestFilterGpuFans(t *testing.T) { + root := t.TempDir() + writeFile(t, filepath.Join(root, "hwmon0", "name"), "xe\n") + writeFile(t, filepath.Join(root, "hwmon0", "fan1_input"), "1200\n") + writeFile(t, filepath.Join(root, "hwmon1", "name"), "nct6798\n") + writeFile(t, filepath.Join(root, "hwmon1", "fan1_input"), "800\n") + + discovered, err := discoverHwmonFans(root) + require.NoError(t, err) + require.Len(t, discovered, 2) + + filtered := filterGpuFans(discovered) + require.Len(t, filtered, 1) + assert.Equal(t, "nct6798_fan1", filtered[0].key) + assert.Len(t, discovered, 2) +} diff --git a/agent/gpu.go b/agent/gpu.go index d1b7b422..270cf4e9 100644 --- a/agent/gpu.go +++ b/agent/gpu.go @@ -454,8 +454,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(), - hasXe: gm.hasXe(), + hasAmdSysfs: gm.hasAmdSysfs(), + hasXe: gm.hasXe(), hasIntelSysfs: gm.hasIntelSysfs(), } if _, err := exec.LookPath(nvidiaSmiCmd); err == nil { @@ -750,9 +750,36 @@ func (gm *GPUManager) resolveLegacyCollectorPriority(caps gpuCapabilities) []col return priorities } +// gpuHwmonChips are hwmon chip names belonging to GPUs. Sensor reads on some +// of these drivers (notably Intel Xe, where each read is a runtime PM resume) +// wake the card, so SKIP_GPU must avoid touching them, not just hide them. +var gpuHwmonChips = []string{"xe", "i915", "amdgpu", "radeon", "nvidia", "nouveau"} + +func isGpuChipName(name string) bool { + name = strings.ToLower(strings.TrimSpace(name)) + for _, chip := range gpuHwmonChips { + if name == chip { + return true + } + } + return false +} + +// SensorKeys are "" or "_