Files

222 lines
6.0 KiB
Go

package gpu
import (
"context"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
const (
amdSMICommand = "amd-smi"
amdSMIDefaultPath = "/opt/rocm/bin/amd-smi"
)
type amdSMI struct {
command string
}
func findAMDSMI() (string, bool) {
for _, command := range []string{amdSMICommand, amdSMIDefaultPath} {
if cmd.Which(command) {
return command, true
}
}
return "", false
}
func (a amdSMI) LoadInfo(ctx context.Context) (*Info, error) {
var (
staticData string
metricData string
processData string
staticErr error
metricErr error
processErr error
wg sync.WaitGroup
)
wg.Add(3)
go func() {
defer wg.Done()
staticData, staticErr = runAMDSMI(ctx, a.command, "static", "--asic", "--bus", "--driver", "--limit", "--json")
}()
go func() {
defer wg.Done()
metricData, metricErr = runAMDSMI(ctx, a.command, "metric", "--usage", "--power", "--temperature", "--mem-usage", "--fan", "--perf-level", "--json")
}()
go func() {
defer wg.Done()
processData, processErr = runAMDSMI(ctx, a.command, "process", "--general", "--json")
}()
wg.Wait()
if staticErr != nil {
return nil, fmt.Errorf("calling %s static failed: %w", a.command, staticErr)
}
info, err := parseAMDStatic(staticData)
if err != nil {
return nil, fmt.Errorf("parsing %s static output failed: %w", a.command, err)
}
if metricErr != nil {
global.LOG.Warnf("calling %s metric failed, metrics will be omitted: %v", a.command, metricErr)
} else if err := applyAMDMetrics(info, metricData); err != nil {
global.LOG.Warnf("parsing %s metric output failed, metrics will be omitted: %v", a.command, err)
}
if processErr != nil {
global.LOG.Warnf("calling %s process failed, process information will be omitted: %v", a.command, processErr)
} else if err := applyAMDProcesses(info, processData); err != nil {
global.LOG.Warnf("parsing %s process output failed, process information will be omitted: %v", a.command, err)
}
return info, nil
}
func runAMDSMI(ctx context.Context, command string, args ...string) (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(10*time.Second))
return cmdMgr.RunWithStdout(command, args...)
}
func parseAMDStatic(data string) (*Info, error) {
rows, err := decodeAMDRows(data)
if err != nil {
return nil, err
}
info := &Info{Type: "amd"}
for _, row := range rows {
index, ok := amdGPUIndex(row)
if !ok {
continue
}
device := Device{
Type: "amd",
Index: index,
ProductName: amdStringAt(row, "asic.market_name", "market_name", "gpu_name"),
PersistenceMode: "N/A",
BusID: amdStringAt(row, "bus.bdf", "bdf"),
DisplayActive: "N/A",
ECC: "N/A",
FanSpeed: "N/A",
Temperature: "N/A",
PerformanceState: "N/A",
PowerDraw: "N/A",
MaxPowerLimit: amdMetricAt(row, "W",
"limit.ppt0.max_power_limit",
"limit.max_power_limit",
"limit.max_power",
),
MemUsed: "N/A",
MemTotal: "N/A",
GPUUtil: "N/A",
ComputeMode: "N/A",
MigMode: "N/A",
}
if device.ProductName == "" {
device.ProductName = "AMD GPU"
}
if device.MaxPowerLimit == "" {
device.MaxPowerLimit = "N/A"
}
if info.DriverVersion == "" {
info.DriverVersion = amdStringAt(row, "driver.version", "driver_version", "amdgpu_version")
}
info.Devices = append(info.Devices, device)
}
sort.Slice(info.Devices, func(i, j int) bool {
return info.Devices[i].Index < info.Devices[j].Index
})
return info, nil
}
func applyAMDMetrics(info *Info, data string) error {
rows, err := decodeAMDRows(data)
if err != nil {
return err
}
devices := amdDevicesByIndex(info)
for _, row := range rows {
index, ok := amdGPUIndex(row)
if !ok {
continue
}
device, ok := devices[index]
if !ok {
continue
}
setAMDMetric(&device.GPUUtil, row, "%", "usage.gfx_activity", "usage.gfx", "gfx_activity", "gfx_usage")
setAMDMetric(&device.Temperature, row, "°C", "temperature.hotspot", "temperature.edge", "hotspot_temperature", "gpu_temperature", "gpu_temp")
setAMDMetric(&device.PowerDraw, row, "W", "power.socket_power", "socket_power", "power_usage")
setAMDMetric(&device.MemUsed, row, "MB", "mem_usage.used_vram", "vram.used", "used_vram", "vram_used")
setAMDMetric(&device.MemTotal, row, "MB", "mem_usage.total_vram", "vram.total", "total_vram", "vram_total")
setAMDMetric(&device.FanSpeed, row, "%", "fan.speed", "fan_speed")
if value := amdStringAt(row, "perf_level", "performance_level"); value != "" {
device.PerformanceState = value
}
}
return nil
}
func applyAMDProcesses(info *Info, data string) error {
rows, err := decodeAMDRows(data)
if err != nil {
return err
}
devices := amdDevicesByIndex(info)
for _, row := range rows {
index, ok := amdGPUIndex(row)
if !ok {
continue
}
device, ok := devices[index]
if !ok {
continue
}
processList, _ := amdValueAt(row, "process_list")
items := amdObjectList(processList)
if len(items) == 0 {
if _, ok := amdValueAt(row, "process_info"); ok {
items = amdSMIRows{row}
}
}
for _, processRow := range items {
pid := amdStringAt(processRow, "process_info.pid", "pid")
if pid == "" || strings.EqualFold(pid, "N/A") {
continue
}
device.Processes = append(device.Processes, Process{
PID: pid,
Type: "C",
ProcessName: amdStringAt(processRow, "process_info.name", "name"),
UsedMemory: amdMetricAt(processRow, "B",
"process_info.mem_usage",
"process_info.mem",
"process_info.memory_usage.vram_mem",
"process_info.vram_mem",
"mem_usage",
),
})
}
}
return nil
}
func amdDevicesByIndex(info *Info) map[uint]*Device {
devices := make(map[uint]*Device, len(info.Devices))
for index := range info.Devices {
devices[info.Devices[index].Index] = &info.Devices[index]
}
return devices
}
func setAMDMetric(target *string, row amdSMIRow, defaultUnit string, paths ...string) {
if value := amdMetricAt(row, defaultUnit, paths...); value != "" {
*target = value
}
}