Skip to content

Commit ff13f10

Browse files
committed
feat: support Ascend 910B GPU monitoring
1 parent b306bfa commit ff13f10

15 files changed

Lines changed: 468 additions & 35 deletions

File tree

agent/app/dto/dashboard.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ type DiskInfo struct {
156156
}
157157

158158
type GPUInfo struct {
159+
Type string `json:"type"`
159160
Index uint `json:"index"`
160161
ProductName string `json:"productName"`
161162
GPUUtil string `json:"gpuUtil"`

agent/app/dto/monitor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type GPUChartHide struct {
4949
GPU bool `json:"gpu"`
5050
Memory bool `json:"memory"`
5151
Power bool `json:"power"`
52+
PowerLimit bool `json:"powerLimit"`
5253
Temperature bool `json:"temperature"`
5354
Speed bool `json:"speed"`
5455
}

agent/app/repo/monitor.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
7373
return global.MonitorDB.Create(&model).Error
7474
}
7575
func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error {
76+
if len(list) == 0 {
77+
return nil
78+
}
7679
return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error
7780
}
7881
func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error {

agent/app/service/monitor.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
155155
if (item.MaxPowerLimit == "" || item.MaxPowerLimit == "N/A") && (item.PowerDraw == "" || item.PowerDraw == "N/A") {
156156
chartHide.Power = true
157157
}
158+
chartHide.PowerLimit = item.MaxPowerLimit == "" || item.MaxPowerLimit == "N/A"
158159
chartHide.Temperature = item.Temperature == "" || item.Temperature == "N/A"
159160
chartHide.Speed = item.FanSpeed == "" || item.FanSpeed == "N/A"
160161
data.ChartHide = append(data.ChartHide, chartHide)
@@ -174,6 +175,7 @@ func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
174175
var chartHide dto.GPUChartHide
175176
chartHide.GPU = true
176177
chartHide.Speed = true
178+
chartHide.PowerLimit = true
177179
chartHide.ProductName = fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName)
178180
if (item.Stats.MemoryUsed == "" || item.Stats.MemoryUsed == "N/A") && (item.Basic.Memory == "" || item.Basic.FreeMemory == "N/A") {
179181
chartHide.Memory = true
@@ -344,6 +346,7 @@ func (m *MonitorService) Run() {
344346
_ = monitorRepo.DelMonitorBase(timeForDelete)
345347
_ = monitorRepo.DelMonitorIO(timeForDelete)
346348
_ = monitorRepo.DelMonitorNet(timeForDelete)
349+
_ = monitorRepo.DelMonitorGPU(timeForDelete)
347350
}
348351

349352
func (m *MonitorService) loadDiskIO() {
@@ -599,6 +602,7 @@ func saveGPUDataToDB() {
599602
}
600603
gpuInfo, err := client.LoadGpuInfo()
601604
if err != nil {
605+
global.LOG.Errorf("load gpu monitor data failed, err: %v", err)
602606
return
603607
}
604608
var list []model.MonitorGPU
@@ -631,6 +635,7 @@ func saveXPUDataToDB() {
631635
}
632636
xpuInfo, err := client.LoadGpuInfo()
633637
if err != nil {
638+
global.LOG.Errorf("load xpu monitor data failed, err: %v", err)
634639
return
635640
}
636641
var list []model.MonitorGPU

agent/utils/ai_tools/gpu/ascend.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package gpu
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strconv"
7+
"strings"
8+
"time"
9+
10+
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common"
11+
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
12+
)
13+
14+
var (
15+
ascendVersionPattern = regexp.MustCompile(`(?i)\bVersion:\s*([^\s|]+)`)
16+
ascendMemoryPattern = regexp.MustCompile(`([0-9]+(?:\.[0-9]+)?)\s*/\s*([0-9]+(?:\.[0-9]+)?)`)
17+
)
18+
19+
type AscendSMI struct{}
20+
21+
func (a AscendSMI) LoadGpuInfo() (*common.GpuInfo, error) {
22+
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(5 * time.Second))
23+
itemData, err := cmdMgr.RunWithStdout("npu-smi", "info")
24+
if err != nil {
25+
return nil, fmt.Errorf("calling npu-smi failed, %v", err)
26+
}
27+
return parseAscendSMI(itemData), nil
28+
}
29+
30+
func parseAscendSMI(data string) *common.GpuInfo {
31+
info := &common.GpuInfo{Type: "ascend"}
32+
if match := ascendVersionPattern.FindStringSubmatch(data); len(match) == 2 {
33+
info.DriverVersion = match[1]
34+
}
35+
36+
processSection := false
37+
deviceIndexes := make(map[uint]int)
38+
var pending *common.GPU
39+
40+
for _, line := range strings.Split(data, "\n") {
41+
if strings.Contains(line, "Process id") && strings.Contains(line, "Process memory") {
42+
processSection = true
43+
pending = nil
44+
continue
45+
}
46+
47+
cells := ascendTableCells(line)
48+
if processSection {
49+
if len(cells) != 4 && len(cells) != 5 {
50+
continue
51+
}
52+
deviceFields := strings.Fields(cells[0])
53+
if len(deviceFields) == 0 {
54+
continue
55+
}
56+
deviceID, err := strconv.ParseUint(deviceFields[0], 10, 64)
57+
if err != nil {
58+
continue
59+
}
60+
index, ok := deviceIndexes[uint(deviceID)]
61+
if !ok {
62+
continue
63+
}
64+
processOffset := len(cells) - 3
65+
info.GPUs[index].Processes = append(info.GPUs[index].Processes, common.Process{
66+
Pid: strings.TrimSpace(cells[processOffset]),
67+
Type: "NPU",
68+
ProcessName: strings.TrimSpace(cells[processOffset+1]),
69+
UsedMemory: ascendValueWithUnit(cells[processOffset+2], "MB"),
70+
})
71+
continue
72+
}
73+
74+
if len(cells) != 3 {
75+
continue
76+
}
77+
if pending == nil {
78+
fields := strings.Fields(cells[0])
79+
if len(fields) < 2 {
80+
continue
81+
}
82+
deviceID, err := strconv.ParseUint(fields[0], 10, 64)
83+
if err != nil {
84+
continue
85+
}
86+
metrics := strings.Fields(cells[2])
87+
if len(metrics) < 2 {
88+
continue
89+
}
90+
pending = &common.GPU{
91+
Type: "ascend",
92+
Index: uint(deviceID),
93+
ProductName: strings.Join(fields[1:], " "),
94+
PersistenceMode: "N/A",
95+
DisplayActive: "N/A",
96+
ECC: "N/A",
97+
FanSpeed: "N/A",
98+
Temperature: ascendValueWithUnit(metrics[1], "C"),
99+
PerformanceState: strings.TrimSpace(cells[1]),
100+
PowerDraw: ascendValueWithUnit(metrics[0], "W"),
101+
MaxPowerLimit: "N/A",
102+
ComputeMode: "N/A",
103+
MigMode: "N/A",
104+
}
105+
continue
106+
}
107+
108+
metrics := strings.Fields(cells[2])
109+
if len(metrics) == 0 {
110+
pending = nil
111+
continue
112+
}
113+
pending.BusID = strings.TrimSpace(cells[1])
114+
pending.GPUUtil = ascendValueWithUnit(metrics[0], "%")
115+
pending.MemUsed, pending.MemTotal = ascendMemoryUsage(cells[2])
116+
deviceIndexes[pending.Index] = len(info.GPUs)
117+
info.GPUs = append(info.GPUs, *pending)
118+
pending = nil
119+
}
120+
121+
return info
122+
}
123+
124+
func ascendTableCells(line string) []string {
125+
line = strings.TrimSpace(line)
126+
if !strings.HasPrefix(line, "|") || !strings.HasSuffix(line, "|") {
127+
return nil
128+
}
129+
parts := strings.Split(line, "|")
130+
if len(parts) < 3 {
131+
return nil
132+
}
133+
cells := make([]string, 0, len(parts)-2)
134+
for _, part := range parts[1 : len(parts)-1] {
135+
cells = append(cells, strings.TrimSpace(part))
136+
}
137+
return cells
138+
}
139+
140+
func ascendMemoryUsage(value string) (string, string) {
141+
matches := ascendMemoryPattern.FindAllStringSubmatch(value, -1)
142+
if len(matches) == 0 {
143+
return "N/A", "N/A"
144+
}
145+
// 910B exposes both generic memory and HBM. Prefer the last memory pool
146+
// with a non-zero capacity, which selects HBM while retaining compatibility
147+
// with devices that only expose Memory-Usage.
148+
selected := matches[len(matches)-1]
149+
for i := len(matches) - 1; i >= 0; i-- {
150+
if total, err := strconv.ParseFloat(matches[i][2], 64); err == nil && total > 0 {
151+
selected = matches[i]
152+
break
153+
}
154+
}
155+
return selected[1] + " MB", selected[2] + " MB"
156+
}
157+
158+
func ascendValueWithUnit(value, unit string) string {
159+
value = strings.TrimSpace(value)
160+
if value == "" || strings.EqualFold(value, "N/A") || strings.EqualFold(value, "NA") {
161+
return "N/A"
162+
}
163+
return value + " " + unit
164+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package gpu
2+
3+
import "testing"
4+
5+
func TestParseAscendSMI910B(t *testing.T) {
6+
output := `+------------------------------------------------------------------------------------------------+
7+
| npu-smi 24.1.rc3 Version: 24.1.rc3 |
8+
+---------------------------+---------------+----------------------------------------------------+
9+
| NPU Name | Health | Power(W) Temp(C) Hugepages-Usage(page)|
10+
| Chip | Bus-Id | AICore(%) Memory-Usage(MB) HBM-Usage(MB) |
11+
+===========================+===============+====================================================+
12+
| 0 910B2C | OK | 92.1 45 0 / 0 |
13+
| 0 | 0000:5A:00.0 | 37 0 / 0 3383 / 65536 |
14+
+===========================+===============+====================================================+
15+
| 16 910 B4 | Warning | 72.1 42 0 / 0 |
16+
| 0 | 0000:15:00.0 | 8 0 / 0 2900 / 32768 |
17+
+===========================+===============+====================================================+
18+
+---------------------------+---------------+----------------------------------------------------+
19+
| NPU Chip | Process id | Process name | Process memory(MB) |
20+
+===========================+===============+====================================================+
21+
| 16 0 | 207848 | python | 116 |
22+
+===========================+===============+====================================================+`
23+
24+
info := parseAscendSMI(output)
25+
if info.Type != "ascend" || info.DriverVersion != "24.1.rc3" {
26+
t.Fatalf("unexpected device info: %#v", info)
27+
}
28+
if len(info.GPUs) != 2 {
29+
t.Fatalf("expected 2 NPUs, got %d", len(info.GPUs))
30+
}
31+
first := info.GPUs[0]
32+
if first.Type != "ascend" || first.Index != 0 || first.ProductName != "910B2C" || first.BusID != "0000:5A:00.0" {
33+
t.Errorf("unexpected first NPU identity: %#v", first)
34+
}
35+
if first.GPUUtil != "37 %" || first.Temperature != "45 C" || first.PowerDraw != "92.1 W" {
36+
t.Errorf("unexpected first NPU metrics: %#v", first)
37+
}
38+
if first.MemUsed != "3383 MB" || first.MemTotal != "65536 MB" {
39+
t.Errorf("expected HBM usage, got %q / %q", first.MemUsed, first.MemTotal)
40+
}
41+
42+
second := info.GPUs[1]
43+
if second.Index != 16 || second.ProductName != "910 B4" || second.PerformanceState != "Warning" {
44+
t.Errorf("unexpected second NPU: %#v", second)
45+
}
46+
if len(second.Processes) != 1 {
47+
t.Fatalf("expected one process, got %d", len(second.Processes))
48+
}
49+
process := second.Processes[0]
50+
if process.Pid != "207848" || process.Type != "NPU" || process.ProcessName != "python" || process.UsedMemory != "116 MB" {
51+
t.Errorf("unexpected process: %#v", process)
52+
}
53+
}
54+
55+
func TestAscendMemoryUsageFallsBackToGenericMemory(t *testing.T) {
56+
used, total := ascendMemoryUsage("12 / 32768")
57+
if used != "12 MB" || total != "32768 MB" {
58+
t.Fatalf("unexpected memory usage: %q / %q", used, total)
59+
}
60+
61+
used, total = ascendMemoryUsage("N/A")
62+
if used != "N/A" || total != "N/A" {
63+
t.Fatalf("unexpected unavailable memory usage: %q / %q", used, total)
64+
}
65+
}

agent/utils/ai_tools/gpu/common/gpu_info.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type GpuInfo struct {
99
}
1010

1111
type GPU struct {
12+
Type string `json:"type"`
1213
Index uint `json:"index"`
1314
ProductName string `json:"productName"`
1415
PersistenceMode string `json:"persistenceMode"`

0 commit comments

Comments
 (0)