intel_gpu_top testing

This commit is contained in:
henrygd
2025-09-16 16:37:44 -04:00
parent 240e75f025
commit 6a406e5206
6 changed files with 132 additions and 28 deletions

View File

@@ -27,13 +27,10 @@ const (
nvidiaSmiInterval string = "4" // in seconds nvidiaSmiInterval string = "4" // in seconds
tegraStatsInterval string = "3700" // in milliseconds tegraStatsInterval string = "3700" // in milliseconds
rocmSmiInterval time.Duration = 4300 * time.Millisecond rocmSmiInterval time.Duration = 4300 * time.Millisecond
// Command retry and timeout constants // Command retry and timeout constants
retryWaitTime time.Duration = 5 * time.Second retryWaitTime time.Duration = 5 * time.Second
maxFailureRetries int = 5 maxFailureRetries int = 5
cmdBufferSize uint16 = 10 * 1024
// Unit Conversions // Unit Conversions
mebibytesInAMegabyte float64 = 1.024 // nvidia-smi reports memory in MiB mebibytesInAMegabyte float64 = 1.024 // nvidia-smi reports memory in MiB
milliwattsInAWatt float64 = 1000.0 // tegrastats reports power in mW milliwattsInAWatt float64 = 1000.0 // tegrastats reports power in mW
@@ -45,6 +42,7 @@ type GPUManager struct {
nvidiaSmi bool nvidiaSmi bool
rocmSmi bool rocmSmi bool
tegrastats bool tegrastats bool
intelGpuStats bool
GpuDataMap map[string]*system.GPUData GpuDataMap map[string]*system.GPUData
} }
@@ -66,6 +64,7 @@ type gpuCollector struct {
cmdArgs []string cmdArgs []string
parse func([]byte) bool // returns true if valid data was found parse func([]byte) bool // returns true if valid data was found
buf []byte buf []byte
bufSize uint16
} }
var errNoValidData = fmt.Errorf("no valid GPU data found") // Error for missing data var errNoValidData = fmt.Errorf("no valid GPU data found") // Error for missing data
@@ -99,7 +98,7 @@ func (c *gpuCollector) collect() error {
scanner := bufio.NewScanner(stdout) scanner := bufio.NewScanner(stdout)
if c.buf == nil { if c.buf == nil {
c.buf = make([]byte, 0, cmdBufferSize) c.buf = make([]byte, 0, c.bufSize)
} }
scanner.Buffer(c.buf, bufio.MaxScanTokenSize) scanner.Buffer(c.buf, bufio.MaxScanTokenSize)
@@ -244,20 +243,24 @@ func (gm *GPUManager) GetCurrentData() map[string]system.GPUData {
// copy / reset the data // copy / reset the data
gpuData := make(map[string]system.GPUData, len(gm.GpuDataMap)) gpuData := make(map[string]system.GPUData, len(gm.GpuDataMap))
for id, gpu := range gm.GpuDataMap { for id, gpu := range gm.GpuDataMap {
gpuAvg := *gpu // avoid division by zero
count := max(gpu.Count, 1)
// average the data
gpuAvg := *gpu
gpuAvg.Temperature = twoDecimals(gpu.Temperature) gpuAvg.Temperature = twoDecimals(gpu.Temperature)
gpuAvg.MemoryUsed = twoDecimals(gpu.MemoryUsed) gpuAvg.MemoryUsed = twoDecimals(gpu.MemoryUsed)
gpuAvg.MemoryTotal = twoDecimals(gpu.MemoryTotal) gpuAvg.MemoryTotal = twoDecimals(gpu.MemoryTotal)
gpuAvg.Usage = twoDecimals(gpu.Usage / count)
// avoid division by zero gpuAvg.Power = twoDecimals(gpu.Power / count)
if gpu.Count > 0 { gpuAvg.Engines = make(map[string]float64, len(gpu.Engines))
gpuAvg.Usage = twoDecimals(gpu.Usage / gpu.Count) for name, engine := range gpu.Engines {
gpuAvg.Power = twoDecimals(gpu.Power / gpu.Count) gpuAvg.Engines[name] = twoDecimals(engine / count)
} }
// reset accumulators in the original // reset accumulators in the original gpu data for next collection
gpu.Usage, gpu.Power, gpu.Count = 0, 0, 0 gpu.Usage, gpu.Power, gpu.Count = gpuAvg.Usage, gpuAvg.Power, 1
gpu.Engines = gpuAvg.Engines
// append id to the name if there are multiple GPUs with the same name // append id to the name if there are multiple GPUs with the same name
if nameCounts[gpu.Name] > 1 { if nameCounts[gpu.Name] > 1 {
@@ -284,18 +287,28 @@ func (gm *GPUManager) detectGPUs() error {
gm.tegrastats = true gm.tegrastats = true
gm.nvidiaSmi = false gm.nvidiaSmi = false
} }
if gm.nvidiaSmi || gm.rocmSmi || gm.tegrastats { if _, err := exec.LookPath(intelGpuStatsCmd); err == nil {
slog.Info("Intel GPU stats found")
gm.intelGpuStats = true
}
if gm.nvidiaSmi || gm.rocmSmi || gm.tegrastats || gm.intelGpuStats {
return nil return nil
} }
return fmt.Errorf("no GPU found - install nvidia-smi, rocm-smi, or tegrastats") return fmt.Errorf("no GPU found - install nvidia-smi, rocm-smi, tegrastats, or intel_gpu_top")
} }
// startCollector starts the appropriate GPU data collector based on the command // startCollector starts the appropriate GPU data collector based on the command
func (gm *GPUManager) startCollector(command string) { func (gm *GPUManager) startCollector(command string) {
collector := gpuCollector{ collector := gpuCollector{
name: command, name: command,
bufSize: 10 * 1024,
} }
switch command { switch command {
case intelGpuStatsCmd:
slog.Info("Starting Intel GPU stats collector")
collector.cmdArgs = []string{"-s", intelGpuStatsInterval, "-J"}
collector.parse = gm.parseIntelData
go collector.start()
case nvidiaSmiCmd: case nvidiaSmiCmd:
collector.cmdArgs = []string{ collector.cmdArgs = []string{
"-l", nvidiaSmiInterval, "-l", nvidiaSmiInterval,
@@ -344,6 +357,9 @@ func NewGPUManager() (*GPUManager, error) {
if gm.tegrastats { if gm.tegrastats {
gm.startCollector(tegraStatsCmd) gm.startCollector(tegraStatsCmd)
} }
if gm.intelGpuStats {
gm.startCollector(intelGpuStatsCmd)
}
return &gm, nil return &gm, nil
} }

53
agent/gpu_intel.go Normal file
View File

@@ -0,0 +1,53 @@
package agent
import (
"encoding/json"
"log/slog"
"github.com/henrygd/beszel/internal/entities/system"
)
const (
intelGpuStatsCmd string = "intel_gpu_top"
intelGpuStatsInterval string = "3800" // in milliseconds
)
type intelGpuStats struct {
Power struct {
GPU float64 `json:"gpu"`
} `json:"power"`
Engines map[string]struct {
Busy float64 `json:"busy"`
} `json:"engines"`
}
func (gm *GPUManager) parseIntelData(output []byte) bool {
slog.Info("Parsing Intel GPU stats")
var intelGpuStats intelGpuStats
if err := json.Unmarshal(output, &intelGpuStats); err != nil {
slog.Error("Error parsing Intel GPU stats", "err", err)
return false
}
gm.Lock()
defer gm.Unlock()
// only one gpu for now - cmd doesn't provide all by default
gpuData, ok := gm.GpuDataMap["0"]
if !ok {
gpuData = &system.GPUData{Name: "GPU", Engines: make(map[string]float64, len(intelGpuStats.Engines))}
gm.GpuDataMap["0"] = gpuData
}
if intelGpuStats.Power.GPU > 0 {
gpuData.Power += intelGpuStats.Power.GPU
}
for name, engine := range intelGpuStats.Engines {
gpuData.Engines[name] += engine.Busy
}
gpuData.Count++
slog.Info("GPU Data", "gpuData", gpuData)
return true
}

View File

@@ -52,6 +52,7 @@ type GPUData struct {
Usage float64 `json:"u" cbor:"3,keyasint"` Usage float64 `json:"u" cbor:"3,keyasint"`
Power float64 `json:"p,omitempty" cbor:"4,keyasint,omitempty"` Power float64 `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
Count float64 `json:"-"` Count float64 `json:"-"`
Engines map[string]float64 `json:"e,omitempty" cbor:"5,keyasint,omitempty"`
} }
type FsStats struct { type FsStats struct {

View File

@@ -115,7 +115,7 @@ export function useNetworkInterfaces(interfaces: SystemStats["ni"]) {
data: (index = 3) => { data: (index = 3) => {
return sortedKeys.map((key) => ({ return sortedKeys.map((key) => ({
label: key, label: key,
dataKey: (stats: SystemStatsRecord) => stats.stats?.ni?.[key]?.[index], dataKey: ({ stats }: SystemStatsRecord) => stats?.ni?.[key]?.[index],
color: `hsl(${220 + (((sortedKeys.indexOf(key) * 360) / sortedKeys.length) % 360)}, 70%, 50%)`, color: `hsl(${220 + (((sortedKeys.indexOf(key) * 360) / sortedKeys.length) % 360)}, 70%, 50%)`,
opacity: 0.3, opacity: 0.3,
@@ -123,3 +123,15 @@ export function useNetworkInterfaces(interfaces: SystemStats["ni"]) {
}, },
} }
} }
/** Generates chart configurations for GPU engines */
export function useGpuEngines(systemStats?: SystemStatsRecord) {
const keys = Object.keys(systemStats?.stats.g?.[0]?.e ?? {})
const sortedKeys = keys.sort()
return sortedKeys.map((engine) => ({
label: engine,
dataKey: ({ stats }: SystemStatsRecord) => stats?.g?.[0]?.e?.[engine] ?? 0,
color: `hsl(${220 + ((sortedKeys.indexOf(engine) * 360) / sortedKeys.length) % 360}, 65%, 52%)`,
opacity: 0.35,
}))
}

View File

@@ -18,7 +18,7 @@ import AreaChartDefault from "@/components/charts/area-chart"
import ContainerChart from "@/components/charts/container-chart" import ContainerChart from "@/components/charts/container-chart"
import DiskChart from "@/components/charts/disk-chart" import DiskChart from "@/components/charts/disk-chart"
import GpuPowerChart from "@/components/charts/gpu-power-chart" import GpuPowerChart from "@/components/charts/gpu-power-chart"
import { useContainerChartConfigs } from "@/components/charts/hooks" import { useContainerChartConfigs, useGpuEngines } from "@/components/charts/hooks"
import LoadAverageChart from "@/components/charts/load-average-chart" import LoadAverageChart from "@/components/charts/load-average-chart"
import MemChart from "@/components/charts/mem-chart" import MemChart from "@/components/charts/mem-chart"
import SwapChart from "@/components/charts/swap-chart" import SwapChart from "@/components/charts/swap-chart"
@@ -61,6 +61,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
import { Separator } from "../ui/separator" import { Separator } from "../ui/separator"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
import NetworkSheet from "./system/network-sheet" import NetworkSheet from "./system/network-sheet"
import LineChartDefault from "../charts/line-chart"
type ChartTimeData = { type ChartTimeData = {
time: number time: number
@@ -398,6 +399,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
const lastGpuVals = Object.values(systemStats.at(-1)?.stats.g ?? {}) const lastGpuVals = Object.values(systemStats.at(-1)?.stats.g ?? {})
const hasGpuData = lastGpuVals.length > 0 const hasGpuData = lastGpuVals.length > 0
const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined) const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined)
const hasGpuEnginesData = lastGpuVals.some((gpu) => gpu.e !== undefined)
let translatedStatus: string = system.status let translatedStatus: string = system.status
if (system.status === SystemStatus.Up) { if (system.status === SystemStatus.Up) {
@@ -770,6 +772,17 @@ export default memo(function SystemDetail({ name }: { name: string }) {
<GpuPowerChart chartData={chartData} /> <GpuPowerChart chartData={chartData} />
</ChartCard> </ChartCard>
)} )}
{hasGpuEnginesData && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`GPU Engines`}
description={t`Average utilization of GPU engines`}
>
<GpuEnginesChart chartData={chartData} />
</ChartCard>
)}
</div> </div>
{/* GPU charts */} {/* GPU charts */}
@@ -897,6 +910,13 @@ export default memo(function SystemDetail({ name }: { name: string }) {
) )
}) })
function GpuEnginesChart({ chartData }: { chartData: ChartData }) {
const engineData = useGpuEngines(chartData.systemStats.at(-1))
return (
<LineChartDefault legend={true} chartData={chartData} dataPoints={engineData} tickFormatter={(val) => `${toFixedFloat(val, 2)}%`} contentFormatter={({ value }) => `${decimalString(value)}%`} />
)
}
function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) { function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
const containerFilter = useStore(store) const containerFilter = useStore(store)
const { t } = useLingui() const { t } = useLingui()

View File

@@ -158,6 +158,8 @@ export interface GPUData {
u: number u: number
/** power (w) */ /** power (w) */
p?: number p?: number
/** engines */
e?: Record<string, number>
} }
export interface ExtraFsStats { export interface ExtraFsStats {