From d62259ffa7b9a9e634900b90afc847d792ab2560 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 6 Nov 2025 00:19:04 +0000 Subject: [PATCH] Add AMD GPU temperature monitoring support Related to #600 - Add GPU field to Temperature model with edge, junction, and mem sensors - Add amdgpu chip recognition to temperature parser - Implement parseGPUTemps() to extract AMD GPU temperature data - Update frontend TypeScript types to include GPU temperatures - Display GPU temps in node table tooltip alongside CPU temps - Set hasGPU flag when GPU data is available This enables temperature monitoring for AMD GPUs (amdgpu sensors) that was previously being collected via SSH but silently discarded during parsing. --- .../components/shared/NodeSummaryTable.tsx | 24 +++++++- frontend-modern/src/types/api.ts | 9 +++ internal/models/models.go | 10 ++++ internal/monitoring/temperature.go | 55 ++++++++++++++++++- 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/frontend-modern/src/components/shared/NodeSummaryTable.tsx b/frontend-modern/src/components/shared/NodeSummaryTable.tsx index dd9534f..b7de817 100644 --- a/frontend-modern/src/components/shared/NodeSummaryTable.tsx +++ b/frontend-modern/src/components/shared/NodeSummaryTable.tsx @@ -628,7 +628,7 @@ export const NodeSummaryTable: Component = (props) => { online && isPVE && cpuTemperatureValue !== null && - (node!.temperature?.hasCPU ?? node!.temperature?.available) && + (node!.temperature?.hasCPU ?? node!.temperature?.hasGPU ?? node!.temperature?.available) && isTemperatureMonitoringEnabled(node!) } fallback={ @@ -653,7 +653,10 @@ export const NodeSummaryTable: Component = (props) => { typeof cpuMax === 'number' && cpuMax > 0; - if (hasMinMax) { + const gpus = temp?.gpu ?? []; + const hasGPU = gpus.length > 0; + + if (hasMinMax || hasGPU) { const min = Math.round(cpuMin); const max = Math.round(cpuMax); @@ -669,7 +672,22 @@ export const NodeSummaryTable: Component = (props) => { {value}°C ); diff --git a/frontend-modern/src/types/api.ts b/frontend-modern/src/types/api.ts index a5eda7f..b207f0f 100644 --- a/frontend-modern/src/types/api.ts +++ b/frontend-modern/src/types/api.ts @@ -752,9 +752,11 @@ export interface Temperature { minRecorded?: string; // When minimum temperature was recorded maxRecorded?: string; // When maximum temperature was recorded cores?: CoreTemp[]; // Individual core temperatures + gpu?: GPUTemp[]; // GPU temperatures nvme?: NVMeTemp[]; // NVMe drive temperatures available: boolean; // Whether any temperature data is available hasCPU?: boolean; // Whether CPU temperature data is available + hasGPU?: boolean; // Whether GPU temperature data is available hasNVMe?: boolean; // Whether NVMe temperature data is available lastUpdate: string; // When this data was collected } @@ -764,6 +766,13 @@ export interface CoreTemp { temp: number; } +export interface GPUTemp { + device: string; // GPU device identifier (e.g., "amdgpu-pci-0400") + edge?: number; // Edge temperature + junction?: number; // Junction/hotspot temperature + mem?: number; // Memory temperature +} + export interface NVMeTemp { device: string; temp: number; diff --git a/internal/models/models.go b/internal/models/models.go index 65dacd7..6c6061c 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -773,9 +773,11 @@ type Temperature struct { MinRecorded time.Time `json:"minRecorded,omitempty"` // When minimum temperature was recorded MaxRecorded time.Time `json:"maxRecorded,omitempty"` // When maximum temperature was recorded Cores []CoreTemp `json:"cores,omitempty"` // Individual core temperatures + GPU []GPUTemp `json:"gpu,omitempty"` // GPU temperatures NVMe []NVMeTemp `json:"nvme,omitempty"` // NVMe drive temperatures Available bool `json:"available"` // Whether any temperature data is available HasCPU bool `json:"hasCPU"` // Whether CPU temperature data is available + HasGPU bool `json:"hasGPU"` // Whether GPU temperature data is available HasNVMe bool `json:"hasNVMe"` // Whether NVMe temperature data is available LastUpdate time.Time `json:"lastUpdate"` // When this data was collected } @@ -786,6 +788,14 @@ type CoreTemp struct { Temp float64 `json:"temp"` } +// GPUTemp represents a GPU temperature sensor +type GPUTemp struct { + Device string `json:"device"` // GPU device identifier (e.g., "amdgpu-pci-0400") + Edge float64 `json:"edge,omitempty"` // Edge temperature + Junction float64 `json:"junction,omitempty"` // Junction/hotspot temperature + Mem float64 `json:"mem,omitempty"` // Memory temperature +} + // NVMeTemp represents an NVMe drive temperature type NVMeTemp struct { Device string `json:"device"` diff --git a/internal/monitoring/temperature.go b/internal/monitoring/temperature.go index 191648a..b9097e1 100644 --- a/internal/monitoring/temperature.go +++ b/internal/monitoring/temperature.go @@ -364,6 +364,14 @@ func (tc *TemperatureCollector) parseSensorsJSON(jsonStr string) (*models.Temper if strings.Contains(chipName, "nvme") { tc.parseNVMeTemps(chipName, chipMap, temp) } + + // Handle GPU temperature sensors + if strings.Contains(chipLower, "amdgpu") { + log.Debug(). + Str("chip", chipName). + Msg("Detected AMD GPU temperature chip") + tc.parseGPUTemps(chipName, chipMap, temp) + } } // If we got CPU temps, calculate max from cores if package not available @@ -379,9 +387,10 @@ func (tc *TemperatureCollector) parseSensorsJSON(jsonStr string) (*models.Temper // This prevents false negatives when sensors report 0°C during resets or temporarily temp.HasCPU = foundCPUChip temp.HasNVMe = len(temp.NVMe) > 0 + temp.HasGPU = len(temp.GPU) > 0 // Available means any temperature data exists (backward compatibility) - temp.Available = temp.HasCPU || temp.HasNVMe + temp.Available = temp.HasCPU || temp.HasNVMe || temp.HasGPU // Log summary of what was detected if !foundCPUChip { @@ -397,10 +406,12 @@ func (tc *TemperatureCollector) parseSensorsJSON(jsonStr string) (*models.Temper log.Debug(). Bool("hasCPU", temp.HasCPU). Bool("hasNVMe", temp.HasNVMe). + Bool("hasGPU", temp.HasGPU). Float64("cpuPackage", temp.CPUPackage). Float64("cpuMax", temp.CPUMax). Int("coreCount", len(temp.Cores)). Int("nvmeCount", len(temp.NVMe)). + Int("gpuCount", len(temp.GPU)). Msg("Temperature data parsed successfully") } @@ -551,6 +562,48 @@ func (tc *TemperatureCollector) parseNVMeTemps(chipName string, chipMap map[stri } } +// parseGPUTemps extracts GPU temperature data from a sensor chip +func (tc *TemperatureCollector) parseGPUTemps(chipName string, chipMap map[string]interface{}, temp *models.Temperature) { + gpuTemp := models.GPUTemp{ + Device: chipName, + } + + // AMD GPU sensors typically have: edge, junction (hotspot), mem + for sensorName, sensorData := range chipMap { + sensorMap, ok := sensorData.(map[string]interface{}) + if !ok { + continue + } + + sensorLower := strings.ToLower(sensorName) + tempVal := extractTempInput(sensorMap) + + if math.IsNaN(tempVal) || tempVal <= 0 { + continue + } + + // Map sensor names to struct fields + if strings.Contains(sensorLower, "edge") { + gpuTemp.Edge = tempVal + } else if strings.Contains(sensorLower, "junction") || strings.Contains(sensorLower, "hotspot") { + gpuTemp.Junction = tempVal + } else if strings.Contains(sensorLower, "mem") { + gpuTemp.Mem = tempVal + } + } + + // Only add GPU entry if we got at least one valid temperature + if gpuTemp.Edge > 0 || gpuTemp.Junction > 0 || gpuTemp.Mem > 0 { + temp.GPU = append(temp.GPU, gpuTemp) + log.Debug(). + Str("device", chipName). + Float64("edge", gpuTemp.Edge). + Float64("junction", gpuTemp.Junction). + Float64("mem", gpuTemp.Mem). + Msg("Parsed GPU temperatures") + } +} + // extractTempInput extracts temperature value from sensor data func extractTempInput(sensorMap map[string]interface{}) float64 { // Look for temp*_input fields