From 63f9368e261a3c7f6087c94318d63f2835bb5212 Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:59:15 +0800 Subject: [PATCH] feat: GPU monitoring data supports persistence (#11051) Refs #9496 --- agent/app/api/v2/monitor.go | 21 + agent/app/dto/monitor.go | 35 +- agent/app/model/monitor.go | 13 + agent/app/repo/monitor.go | 27 + agent/app/repo/setting.go | 26 - agent/app/service/monitor.go | 167 +++++- agent/global/global.go | 11 +- agent/init/db/db.go | 1 + agent/init/migration/migrate.go | 1 + agent/init/migration/migrations/init.go | 7 + agent/router/ro_host.go | 1 + frontend/src/api/interface/host.ts | 32 ++ frontend/src/api/modules/host.ts | 3 + frontend/src/views/ai/gpu/index.vue | 651 +++++++++++++----------- 14 files changed, 654 insertions(+), 342 deletions(-) diff --git a/agent/app/api/v2/monitor.go b/agent/app/api/v2/monitor.go index 9473f6cb7..bc57bd019 100644 --- a/agent/app/api/v2/monitor.go +++ b/agent/app/api/v2/monitor.go @@ -31,6 +31,27 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) { helper.SuccessWithData(c, data) } +// @Tags Monitor +// @Summary Load monitor data +// @Param request body dto.MonitorGPUSearch true "request" +// @Success 200 {object} dto.dto.MonitorGPUData +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/monitor/gpu/search [post] +func (b *BaseApi) LoadGPUMonitor(c *gin.Context) { + var req dto.MonitorGPUSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := monitorService.LoadGPUMonitorData(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} + // @Tags Monitor // @Summary Clean monitor data // @Success 200 diff --git a/agent/app/dto/monitor.go b/agent/app/dto/monitor.go index 614cf4e45..393e2cdf4 100644 --- a/agent/app/dto/monitor.go +++ b/agent/app/dto/monitor.go @@ -11,7 +11,7 @@ type MonitorSearch struct { } type MonitorData struct { - Param string `json:"param" validate:"required,oneof=cpu memory load io network"` + Param string `json:"param"` Date []time.Time `json:"date"` Value []interface{} `json:"value"` } @@ -37,3 +37,36 @@ type MonitorSettingUpdate struct { Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"` Value string `json:"value"` } + +type MonitorGPUSearch struct { + ProductName string `json:"productName"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` +} +type MonitorGPUData struct { + ProductNames []string `json:"productNames"` + Date []time.Time `json:"date"` + GPUValue []float64 `json:"gpuValue"` + TemperatureValue []int `json:"temperatureValue"` + PowerValue []GPUPowerUsageHelper `json:"powerValue"` + MemoryValue []GPUMemoryUsageHelper `json:"memoryValue"` + SpeedValue []int `json:"speedValue"` +} +type GPUPowerUsageHelper struct { + Total float64 `json:"total"` + Used float64 `json:"used"` + Percent float64 `json:"percent"` +} +type GPUMemoryUsageHelper struct { + Total int `json:"total"` + Used int `json:"used"` + Percent float64 `json:"percent"` + + GPUProcesses []GPUProcess `json:"gpuProcesses"` +} +type GPUProcess struct { + Pid string `json:"pid"` + Type string `json:"type"` + ProcessName string `json:"processName"` + UsedMemory string `json:"usedMemory"` +} diff --git a/agent/app/model/monitor.go b/agent/app/model/monitor.go index 5a2c594b5..cc825eb5a 100644 --- a/agent/app/model/monitor.go +++ b/agent/app/model/monitor.go @@ -31,3 +31,16 @@ type MonitorNetwork struct { Up float64 `json:"up"` Down float64 `json:"down"` } + +type MonitorGPU struct { + BaseModel + ProductName string `json:"productName"` + GPUUtil float64 `json:"gpuUtil"` + Temperature int `json:"temperature"` + PowerDraw float64 `json:"powerDraw"` + MaxPowerLimit float64 `json:"maxPowerLimit"` + MemUsed int `json:"memUsed"` + MemTotal int `json:"memTotal"` + FanSpeed int `json:"fanSpeed"` + Processes string `json:"processes"` +} diff --git a/agent/app/repo/monitor.go b/agent/app/repo/monitor.go index 5cdca6769..64d9c0ec6 100644 --- a/agent/app/repo/monitor.go +++ b/agent/app/repo/monitor.go @@ -5,21 +5,27 @@ import ( "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/global" + "gorm.io/gorm" ) type MonitorRepo struct{} type IMonitorRepo interface { GetBase(opts ...DBOption) ([]model.MonitorBase, error) + GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) GetIO(opts ...DBOption) ([]model.MonitorIO, error) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error) CreateMonitorBase(model model.MonitorBase) error + BatchCreateMonitorGPU(list []model.MonitorGPU) error BatchCreateMonitorIO(ioList []model.MonitorIO) error BatchCreateMonitorNet(ioList []model.MonitorNetwork) error DelMonitorBase(timeForDelete time.Time) error + DelMonitorGPU(timeForDelete time.Time) error DelMonitorIO(timeForDelete time.Time) error DelMonitorNet(timeForDelete time.Time) error + + WithByProductName(name string) DBOption } func NewIMonitorRepo() IMonitorRepo { @@ -53,10 +59,22 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro err := db.Find(&data).Error return data, err } +func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) { + var data []model.MonitorGPU + db := global.GPUMonitorDB + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&data).Error + return data, err +} func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error { return global.MonitorDB.Create(&model).Error } +func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error { + return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error +} func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error { return global.MonitorDB.CreateInBatches(ioList, len(ioList)).Error } @@ -72,3 +90,12 @@ func (u *MonitorRepo) DelMonitorIO(timeForDelete time.Time) error { func (u *MonitorRepo) DelMonitorNet(timeForDelete time.Time) error { return global.MonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorNetwork{}).Error } +func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error { + return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error +} + +func (s *MonitorRepo) WithByProductName(name string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("product_name = ?", name) + } +} diff --git a/agent/app/repo/setting.go b/agent/app/repo/setting.go index af9db0c8c..429d787be 100644 --- a/agent/app/repo/setting.go +++ b/agent/app/repo/setting.go @@ -2,7 +2,6 @@ package repo import ( "errors" - "time" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/global" @@ -19,12 +18,6 @@ type ISettingRepo interface { Update(key, value string) error WithByKey(key string) DBOption - CreateMonitorBase(model model.MonitorBase) error - BatchCreateMonitorIO(ioList []model.MonitorIO) error - BatchCreateMonitorNet(ioList []model.MonitorNetwork) error - DelMonitorBase(timeForDelete time.Time) error - DelMonitorIO(timeForDelete time.Time) error - DelMonitorNet(timeForDelete time.Time) error UpdateOrCreate(key, value string) error GetDescription(opts ...DBOption) (model.CommonDescription, error) @@ -85,25 +78,6 @@ func (s *SettingRepo) Update(key, value string) error { return global.DB.Model(&model.Setting{}).Where("key = ?", key).Updates(map[string]interface{}{"value": value}).Error } -func (s *SettingRepo) CreateMonitorBase(model model.MonitorBase) error { - return global.MonitorDB.Create(&model).Error -} -func (s *SettingRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error { - return global.MonitorDB.CreateInBatches(ioList, len(ioList)).Error -} -func (s *SettingRepo) BatchCreateMonitorNet(ioList []model.MonitorNetwork) error { - return global.MonitorDB.CreateInBatches(ioList, len(ioList)).Error -} -func (s *SettingRepo) DelMonitorBase(timeForDelete time.Time) error { - return global.MonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorBase{}).Error -} -func (s *SettingRepo) DelMonitorIO(timeForDelete time.Time) error { - return global.MonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorIO{}).Error -} -func (s *SettingRepo) DelMonitorNet(timeForDelete time.Time) error { - return global.MonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorNetwork{}).Error -} - func (s *SettingRepo) UpdateOrCreate(key, value string) error { var setting model.Setting result := global.DB.Where("key = ?", key).First(&setting) diff --git a/agent/app/service/monitor.go b/agent/app/service/monitor.go index 775861c04..f76050172 100644 --- a/agent/app/service/monitor.go +++ b/agent/app/service/monitor.go @@ -6,6 +6,7 @@ import ( "fmt" "sort" "strconv" + "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/repo" @@ -15,6 +16,8 @@ import ( "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu" "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/robfig/cron/v3" "github.com/shirou/gopsutil/v4/cpu" @@ -35,6 +38,7 @@ var monitorCancel context.CancelFunc type IMonitorService interface { Run() LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorData, error) + LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error) LoadSetting() (*dto.MonitorSetting, error) UpdateSetting(key, value string) error CleanData() error @@ -113,6 +117,67 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa return data, nil } +func (m *MonitorService) LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error) { + loc, _ := time.LoadLocation(common.LoadTimeZoneByCmd()) + req.StartTime = req.StartTime.In(loc) + req.EndTime = req.EndTime.In(loc) + + var data dto.MonitorGPUData + gpuExist, gpuclient := gpu.New() + xpuExist, xpuClient := xpu.New() + if !gpuExist && !xpuExist { + return data, nil + } + if len(req.ProductName) == 0 { + if gpuExist { + gpuInfo, err := gpuclient.LoadGpuInfo() + if err != nil || len(gpuInfo.GPUs) == 0 { + return data, buserr.New("ErrRecordNotFound") + } + req.ProductName = gpuInfo.GPUs[0].ProductName + for _, item := range gpuInfo.GPUs { + data.ProductNames = append(data.ProductNames, item.ProductName) + } + } else { + xpuInfo, err := xpuClient.LoadGpuInfo() + if err != nil || len(xpuInfo.Xpu) == 0 { + return data, buserr.New("ErrRecordNotFound") + } + req.ProductName = xpuInfo.Xpu[0].Basic.DeviceName + for _, item := range xpuInfo.Xpu { + data.ProductNames = append(data.ProductNames, item.Basic.DeviceName) + } + } + } + gpuList, err := monitorRepo.GetGPU(repo.WithByCreatedAt(req.StartTime, req.EndTime), monitorRepo.WithByProductName(req.ProductName)) + if err != nil { + return data, err + } + + for _, gpu := range gpuList { + data.Date = append(data.Date, gpu.CreatedAt) + data.GPUValue = append(data.GPUValue, gpu.GPUUtil) + data.TemperatureValue = append(data.TemperatureValue, gpu.Temperature) + data.PowerValue = append(data.PowerValue, dto.GPUPowerUsageHelper{ + Total: gpu.MaxPowerLimit, + Used: gpu.PowerDraw, + Percent: gpu.PowerDraw / gpu.MaxPowerLimit * 100, + }) + memItem := dto.GPUMemoryUsageHelper{ + Total: gpu.MemTotal, + Used: gpu.MemUsed, + Percent: float64(gpu.MemUsed) / float64(gpu.MemTotal) * 100, + } + var process []dto.GPUProcess + if err := json.Unmarshal([]byte(gpu.Processes), &process); err == nil { + memItem.GPUProcesses = process + } + data.MemoryValue = append(data.MemoryValue, memItem) + data.SpeedValue = append(data.SpeedValue, gpu.FanSpeed) + } + return data, nil +} + func (m *MonitorService) LoadSetting() (*dto.MonitorSetting, error) { setting, err := settingRepo.GetList() if err != nil { @@ -174,10 +239,13 @@ func (m *MonitorService) CleanData() error { if err := global.MonitorDB.Exec("DELETE FROM monitor_networks").Error; err != nil { return err } + _ = global.GPUMonitorDB.Exec("DELETE FROM monitor_gpus").Error return nil } func (m *MonitorService) Run() { + saveGPUDataToDB() + saveXPUDataToDB() var itemModel model.MonitorBase totalPercent, _ := cpu.Percent(3*time.Second, false) if len(totalPercent) == 1 { @@ -207,7 +275,7 @@ func (m *MonitorService) Run() { } } - if err := settingRepo.CreateMonitorBase(itemModel); err != nil { + if err := monitorRepo.CreateMonitorBase(itemModel); err != nil { global.LOG.Errorf("Insert basic monitoring data failed, err: %v", err) } @@ -220,9 +288,9 @@ func (m *MonitorService) Run() { } storeDays, _ := strconv.Atoi(MonitorStoreDays.Value) timeForDelete := time.Now().AddDate(0, 0, -storeDays) - _ = settingRepo.DelMonitorBase(timeForDelete) - _ = settingRepo.DelMonitorIO(timeForDelete) - _ = settingRepo.DelMonitorNet(timeForDelete) + _ = monitorRepo.DelMonitorBase(timeForDelete) + _ = monitorRepo.DelMonitorIO(timeForDelete) + _ = monitorRepo.DelMonitorNet(timeForDelete) } func (m *MonitorService) loadDiskIO() { @@ -302,7 +370,7 @@ func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) { } } } - if err := settingRepo.BatchCreateMonitorIO(ioList); err != nil { + if err := monitorRepo.BatchCreateMonitorIO(ioList); err != nil { global.LOG.Errorf("Insert io monitoring data failed, err: %v", err) } m.DiskIO <- ioStat2 @@ -341,7 +409,7 @@ func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64) } } - if err := settingRepo.BatchCreateMonitorNet(netList); err != nil { + if err := monitorRepo.BatchCreateMonitorNet(netList); err != nil { global.LOG.Errorf("Insert network monitoring data failed, err: %v", err) } m.NetIO <- netStat2 @@ -482,3 +550,90 @@ func StartMonitor(removeBefore bool, interval string) error { return nil } + +func saveGPUDataToDB() { + exist, client := gpu.New() + if !exist { + return + } + gpuInfo, err := client.LoadGpuInfo() + if err != nil { + return + } + var list []model.MonitorGPU + for _, gpuItem := range gpuInfo.GPUs { + item := model.MonitorGPU{ + ProductName: gpuItem.ProductName, + GPUUtil: loadGPUInfoFloat(gpuItem.GPUUtil), + Temperature: loadGPUInfoInt(gpuItem.Temperature), + PowerDraw: loadGPUInfoFloat(gpuItem.PowerDraw), + MaxPowerLimit: loadGPUInfoFloat(gpuItem.MaxPowerLimit), + MemUsed: loadGPUInfoInt(gpuItem.MemUsed), + MemTotal: loadGPUInfoInt(gpuItem.MemTotal), + FanSpeed: loadGPUInfoInt(gpuItem.FanSpeed), + } + process, _ := json.Marshal(gpuItem.Processes) + if len(process) != 0 { + item.Processes = string(process) + } + list = append(list, item) + } + if err := repo.NewIMonitorRepo().BatchCreateMonitorGPU(list); err != nil { + global.LOG.Errorf("batch create gpu monitor data failed, err: %v", err) + return + } +} +func saveXPUDataToDB() { + exist, client := xpu.New() + if !exist { + return + } + xpuInfo, err := client.LoadGpuInfo() + if err != nil { + return + } + var list []model.MonitorGPU + for _, xpuItem := range xpuInfo.Xpu { + item := model.MonitorGPU{ + ProductName: xpuItem.Basic.DeviceName, + GPUUtil: loadGPUInfoFloat(xpuItem.Stats.MemoryUtil), + Temperature: loadGPUInfoInt(xpuItem.Stats.Temperature), + PowerDraw: loadGPUInfoFloat(xpuItem.Stats.Power), + MemUsed: loadGPUInfoInt(xpuItem.Stats.MemoryUsed), + MemTotal: loadGPUInfoInt(xpuItem.Basic.Memory), + } + var processItem []dto.GPUProcess + for _, ps := range xpuItem.Processes { + processItem = append(processItem, dto.GPUProcess{ + Pid: fmt.Sprintf("%v", ps.PID), + Type: ps.SHR, + ProcessName: ps.Command, + UsedMemory: ps.Memory, + }) + } + process, _ := json.Marshal(processItem) + if len(process) != 0 { + item.Processes = string(process) + } + list = append(list, item) + } + if err := repo.NewIMonitorRepo().BatchCreateMonitorGPU(list); err != nil { + global.LOG.Errorf("batch create gpu monitor data failed, err: %v", err) + return + } +} +func loadGPUInfoInt(val string) int { + valItem := strings.ReplaceAll(val, "MiB", "") + valItem = strings.ReplaceAll(valItem, "C", "") + valItem = strings.ReplaceAll(valItem, "%", "") + valItem = strings.TrimSpace(valItem) + data, _ := strconv.Atoi(valItem) + return data +} +func loadGPUInfoFloat(val string) float64 { + valItem := strings.ReplaceAll(val, "W", "") + valItem = strings.ReplaceAll(valItem, "%", "") + valItem = strings.TrimSpace(valItem) + data, _ := strconv.ParseFloat(valItem, 64) + return data +} diff --git a/agent/global/global.go b/agent/global/global.go index 8548e2c14..efff79643 100644 --- a/agent/global/global.go +++ b/agent/global/global.go @@ -13,11 +13,12 @@ import ( ) var ( - DB *gorm.DB - MonitorDB *gorm.DB - TaskDB *gorm.DB - CoreDB *gorm.DB - AlertDB *gorm.DB + DB *gorm.DB + MonitorDB *gorm.DB + GPUMonitorDB *gorm.DB + TaskDB *gorm.DB + CoreDB *gorm.DB + AlertDB *gorm.DB LOG *logrus.Logger CONF ServerConfig diff --git a/agent/init/db/db.go b/agent/init/db/db.go index e91f6bb41..af30bf1ba 100644 --- a/agent/init/db/db.go +++ b/agent/init/db/db.go @@ -11,6 +11,7 @@ func Init() { global.DB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "agent.db"), "agent") global.TaskDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "task.db"), "task") global.MonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "monitor.db"), "monitor") + global.GPUMonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "gpu_monitor.db"), "gpu_monitor") global.AlertDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "alert.db"), "alert") if global.IsMaster { diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 15c24199d..c3f025870 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -54,6 +54,7 @@ func InitAgentDB() { migrations.AddIptablesFilterRuleTable, migrations.AddCommonDescription, migrations.UpdateDatabase, + migrations.AddGPUMonitor, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 96e4abf3d..e640fdaff 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -719,3 +719,10 @@ var UpdateDatabase = &gormigrate.Migration{ return tx.AutoMigrate(&model.Database{}) }, } + +var AddGPUMonitor = &gormigrate.Migration{ + ID: "20251119-add-gpu-monitor", + Migrate: func(tx *gorm.DB) error { + return global.GPUMonitorDB.AutoMigrate(&model.MonitorGPU{}) + }, +} diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go index 5ac8d30f8..5c00efc85 100644 --- a/agent/router/ro_host.go +++ b/agent/router/ro_host.go @@ -29,6 +29,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.POST("/firewall/filter/chain/status", baseApi.LoadChainStatus) hostRouter.POST("/monitor/search", baseApi.LoadMonitor) + hostRouter.POST("/monitor/gpu/search", baseApi.LoadGPUMonitor) hostRouter.POST("/monitor/clean", baseApi.CleanMonitor) hostRouter.GET("/monitor/netoptions", baseApi.GetNetworkOptions) hostRouter.GET("/monitor/iooptions", baseApi.GetIOOptions) diff --git a/frontend/src/api/interface/host.ts b/frontend/src/api/interface/host.ts index 934159274..ac47d7f6d 100644 --- a/frontend/src/api/interface/host.ts +++ b/frontend/src/api/interface/host.ts @@ -161,6 +161,38 @@ export namespace Host { endTime: Date; } + export interface MonitorGPUSearch { + productName: string; + startTime: Date; + endTime: Date; + } + export interface MonitorGPUData { + productNames: Array; + date: Array; + gpuValue: Array; + temperatureValue: Array; + powerValue: Array; + memoryValue: Array; + speedValue: Array; + } + export interface GPUPowerUsageHelper { + total: number; + used: number; + percent: number; + } + export interface GPUMemoryUsageHelper { + total: number; + used: number; + percent: number; + gpuProcesses: Array; + } + export interface GPUProcess { + pid: string; + type: string; + processName: string; + usedMemory: string; + } + export interface SSHInfo { autoStart: boolean; isActive: boolean; diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index 500cfdc35..2ae3b6f73 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -65,6 +65,9 @@ export const operateFilterChain = (name: string, op: string) => { export const loadMonitor = (param: Host.MonitorSearch) => { return http.post>(`/hosts/monitor/search`, param); }; +export const loadGPUMonitor = (param: Host.MonitorGPUSearch) => { + return http.post(`/hosts/monitor/gpu/search`, param); +}; export const getNetworkOptions = () => { return http.get>(`/hosts/monitor/netoptions`); }; diff --git a/frontend/src/views/ai/gpu/index.vue b/frontend/src/views/ai/gpu/index.vue index bade2b2bd..d4a9c1bc7 100644 --- a/frontend/src/views/ai/gpu/index.vue +++ b/frontend/src/views/ai/gpu/index.vue @@ -1,5 +1,5 @@