feat: add vLLM monitoring (#13942)

This commit is contained in:
ssongliu
2026-09-29 15:19:30 +08:00
committed by GitHub
parent d27d6db1ea
commit d0183f460e
39 changed files with 1744 additions and 197 deletions
+8 -3
View File
@@ -32,14 +32,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
}
// @Tags Monitor
// @Summary Clean monitor data
// @Summary Clean host or GPU monitor data
// @Param request body dto.MonitorClean true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/monitor/clean [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"}
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 [type] 监控数据","formatEN":"clean [type] monitoring data"}
func (b *BaseApi) CleanMonitor(c *gin.Context) {
if err := monitorService.CleanData(); err != nil {
var req dto.MonitorClean
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := monitorService.CleanData(req.Type); err != nil {
helper.InternalServer(c, err)
return
}
+45
View File
@@ -0,0 +1,45 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
var req dto.MonitorVLLMSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := monitorService.LoadVLLMMonitorData(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) LoadVLLMCurrent(c *gin.Context) {
var req dto.MonitorVLLMCurrent
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := monitorService.LoadVLLMCurrent(c.Request.Context(), req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
func (b *BaseApi) CleanVLLMMonitor(c *gin.Context) {
var req dto.MonitorVLLMClean
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := monitorService.CleanVLLMMonitor(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+43 -7
View File
@@ -1,6 +1,10 @@
package dto
import "time"
import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
type MonitorSearch struct {
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
@@ -26,19 +30,26 @@ type Process struct {
}
type MonitorSetting struct {
MonitorStatus string `json:"monitorStatus"`
MonitorStoreDays string `json:"monitorStoreDays"`
MonitorInterval string `json:"monitorInterval"`
DefaultNetwork string `json:"defaultNetwork"`
DefaultIO string `json:"defaultIO"`
GPUMonitorStatus string `json:"gpuMonitorStatus"`
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
GPUMonitorInterval string `json:"gpuMonitorInterval"`
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
MonitorStatus string `json:"monitorStatus"`
MonitorStoreDays string `json:"monitorStoreDays"`
MonitorInterval string `json:"monitorInterval"`
DefaultNetwork string `json:"defaultNetwork"`
DefaultIO string `json:"defaultIO"`
}
type MonitorSettingUpdate struct {
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"`
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval GPUMonitorStatus GPUMonitorStoreDays GPUMonitorInterval VLLMMonitorStatus VLLMMonitorStoreDays VLLMMonitorInterval DefaultNetwork DefaultIO"`
Value string `json:"value"`
}
type MonitorGPUOptions struct {
Supported bool `json:"supported"`
GPUType string `json:"gpuType"`
ChartHide []GPUChartHide `json:"chartHide"`
Options []string `json:"options"`
@@ -114,3 +125,28 @@ type GPUProcess struct {
ProcessName string `json:"processName"`
UsedMemory string `json:"usedMemory"`
}
type MonitorVLLMSearch struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
StartTime time.Time `json:"startTime" validate:"required"`
EndTime time.Time `json:"endTime" validate:"required"`
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
}
type MonitorVLLMData struct {
SampleCount int64 `json:"sampleCount"`
BucketSeconds int64 `json:"bucketSeconds"`
Points []model.MonitorVLLM `json:"points"`
}
type MonitorVLLMCurrent struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorVLLMClean struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorClean struct {
Type string `json:"type" validate:"required,oneof=host gpu"`
}
+35
View File
@@ -1,5 +1,7 @@
package model
import "time"
type MonitorBase struct {
BaseModel
Cpu float64 `json:"cpu"`
@@ -75,3 +77,36 @@ type MonitorGPU struct {
FanSpeed *float64 `json:"fanSpeed"`
Processes string `json:"processes"`
}
type MonitorVLLM struct {
ID uint `json:"-" gorm:"primarykey;autoIncrement"`
CreatedAt time.Time `json:"createdAt"`
AppInstallID uint `json:"appInstallID"`
Status string `json:"status"`
RawMetrics string `json:"-"`
HistogramDeltas string `json:"-"`
Running *float64 `json:"running"`
Waiting *float64 `json:"waiting"`
CacheUsage *float64 `json:"cacheUsage"`
PromptThroughput *float64 `json:"promptThroughput"`
GenerationThroughput *float64 `json:"generationThroughput"`
RequestThroughput *float64 `json:"requestThroughput"`
TimeToFirstToken *float64 `json:"timeToFirstToken"`
TimePerOutputToken *float64 `json:"timePerOutputToken"`
RequestLatency *float64 `json:"requestLatency"`
PrefillTime *float64 `json:"prefillTime"`
DecodeTime *float64 `json:"decodeTime"`
TimeToFirstTokenP50 *float64 `json:"timeToFirstTokenP50"`
TimeToFirstTokenP90 *float64 `json:"timeToFirstTokenP90"`
TimeToFirstTokenP95 *float64 `json:"timeToFirstTokenP95"`
TimeToFirstTokenP99 *float64 `json:"timeToFirstTokenP99"`
TimePerOutputTokenP50 *float64 `json:"timePerOutputTokenP50"`
TimePerOutputTokenP90 *float64 `json:"timePerOutputTokenP90"`
TimePerOutputTokenP95 *float64 `json:"timePerOutputTokenP95"`
TimePerOutputTokenP99 *float64 `json:"timePerOutputTokenP99"`
RequestLatencyP50 *float64 `json:"requestLatencyP50"`
RequestLatencyP90 *float64 `json:"requestLatencyP90"`
RequestLatencyP95 *float64 `json:"requestLatencyP95"`
RequestLatencyP99 *float64 `json:"requestLatencyP99"`
}
+15 -18
View File
@@ -21,8 +21,9 @@ type GPUHistoryPoint struct {
}
type IMonitorRepo interface {
CleanHost() error
CleanGPU() error
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
GetGPU(opts ...DBOption) ([]model.MonitorGPU, error)
CountGPU(opts ...DBOption) (int64, error)
GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error)
GetGPUDevices() ([]model.MonitorGPU, error)
@@ -38,7 +39,6 @@ type IMonitorRepo interface {
DelMonitorIO(timeForDelete time.Time) error
DelMonitorNet(timeForDelete time.Time) error
WithByProductName(name string) DBOption
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
}
@@ -46,6 +46,19 @@ func NewIMonitorRepo() IMonitorRepo {
return &MonitorRepo{}
}
func (s *MonitorRepo) CleanHost() error {
for _, item := range []interface{}{&model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}} {
if err := global.MonitorDB.Where("1 = 1").Delete(item).Error; err != nil {
return err
}
}
return nil
}
func (s *MonitorRepo) CleanGPU() error {
return global.GPUMonitorDB.Where("1 = 1").Delete(&model.MonitorGPU{}).Error
}
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
var data []model.MonitorBase
db := global.MonitorDB
@@ -73,16 +86,6 @@ 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
}
@@ -111,12 +114,6 @@ 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)
}
}
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
var data []model.MonitorGPU
err := global.GPUMonitorDB.Model(&model.MonitorGPU{}).Select("device_id, product_name, device_type").Group("device_id, product_name, device_type").Order("product_name, device_id").Find(&data).Error
+78
View File
@@ -0,0 +1,78 @@
package repo
import (
"errors"
"fmt"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type VLLMMonitorRepo struct{}
type VLLMHistoryPoint struct {
model.MonitorVLLM
Bucket int64
HistogramSamples string
}
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
return global.VLLMMonitorDB.Create(point).Error
}
func (r *VLLMMonitorRepo) Latest(id uint) (model.MonitorVLLM, error) {
var point model.MonitorVLLM
db := global.VLLMMonitorDB.Where("app_install_id = ?", id)
err := db.Order("created_at DESC, id DESC").First(&point).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return point, nil
}
return point, err
}
func (r *VLLMMonitorRepo) CleanTarget(id uint) error {
return global.VLLMMonitorDB.Where("app_install_id = ?", id).Delete(&model.MonitorVLLM{}).Error
}
func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
}
func (r *VLLMMonitorRepo) Count(id uint, start, end time.Time) (int64, error) {
var count int64
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
err := db.Count(&count).Error
return count, err
}
func (r *VLLMMonitorRepo) History(id uint, start, end time.Time, seconds int64, aggregation string) ([]VLLMHistoryPoint, error) {
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
metrics := []string{"running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99"}
var columns []string
if seconds > 0 {
operation := "AVG"
if aggregation == "max" {
operation = "MAX"
}
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
for _, column := range metrics {
if aggregation != "max" && (strings.HasPrefix(column, "time_to_first_token_p") || strings.HasPrefix(column, "time_per_output_token_p") || strings.HasPrefix(column, "request_latency_p")) {
continue
}
columns = append(columns, operation+"("+column+") AS "+column)
}
if aggregation != "max" {
columns = append(columns, "json_group_array(json(NULLIF(histogram_deltas, ''))) AS histogram_samples")
}
db = db.Group("bucket").Order("bucket ASC")
} else {
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
db = db.Order("created_at ASC, id ASC")
}
var points []VLLMHistoryPoint
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
return points, err
}
+5 -4
View File
@@ -32,10 +32,11 @@ var (
scriptRepo = repo.NewIScriptRepo()
cronjobRepo = repo.NewICronjobRepo()
hostRepo = repo.NewIHostRepo()
ftpRepo = repo.NewIFtpRepo()
clamRepo = repo.NewIClamRepo()
monitorRepo = repo.NewIMonitorRepo()
hostRepo = repo.NewIHostRepo()
ftpRepo = repo.NewIFtpRepo()
clamRepo = repo.NewIClamRepo()
monitorRepo = repo.NewIMonitorRepo()
vllmMonitorRepo = &repo.VLLMMonitorRepo{}
settingRepo = repo.NewISettingRepo()
forwardingRuleRepo = repo.NewIForwardingRuleRepo()
+184 -49
View File
@@ -33,11 +33,14 @@ import (
type MonitorService struct {
DiskIO chan ([]disk.IOCountersStat)
NetIO chan ([]net.IOCountersStat)
ctx context.Context
}
var (
monitorCancel context.CancelFunc
hostSysPath = loadHostSysPath()
monitorSettingMutex sync.Mutex
gpuMonitorMutex sync.Mutex
monitorCancel context.CancelFunc
hostSysPath = loadHostSysPath()
blockDevicePartitionCache sync.Map
)
@@ -47,8 +50,11 @@ type IMonitorService interface {
LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorData, error)
LoadSetting() (*dto.MonitorSetting, error)
UpdateSetting(key, value string) error
CleanData() error
CleanData(monitorType string) error
LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error)
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
LoadGPUOptions() dto.MonitorGPUOptions
LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error)
@@ -60,6 +66,7 @@ func NewIMonitorService() IMonitorService {
return &MonitorService{
DiskIO: make(chan []disk.IOCountersStat, 2),
NetIO: make(chan []net.IOCountersStat, 2),
ctx: context.Background(),
}
}
@@ -128,18 +135,21 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
var data dto.MonitorGPUOptions
exist, client := accelerator.New()
if !exist {
return data
}
seen := make(map[string]bool)
if exist, client := accelerator.New(); exist {
snapshot, err := client.Collect(context.Background())
if err != nil {
global.LOG.Warnf("Load accelerator options failed: %v", err)
} else {
data = loadGPUOptions(snapshot)
for _, item := range data.ChartHide {
seen[item.DeviceID] = true
}
snapshot, err := client.Collect(context.Background())
if err != nil {
global.LOG.Warnf("Load accelerator options failed: %v", err)
} else {
data = loadGPUOptions(snapshot)
for _, item := range data.ChartHide {
seen[item.DeviceID] = true
}
}
data.Supported = true
devices, err := monitorRepo.GetGPUDevices()
if err != nil {
global.LOG.Warnf("Load accelerator history options failed: %v", err)
@@ -282,9 +292,27 @@ func (m *MonitorService) LoadSetting() (*dto.MonitorSetting, error) {
}
func (m *MonitorService) UpdateSetting(key, value string) error {
monitorSettingMutex.Lock()
defer monitorSettingMutex.Unlock()
switch key {
case "MonitorStatus", "GPUMonitorStatus", "VLLMMonitorStatus":
if value != constant.StatusEnable && value != constant.StatusDisable {
return fmt.Errorf("invalid monitoring status")
}
case "MonitorInterval", "GPUMonitorInterval", "VLLMMonitorInterval":
interval, err := strconv.Atoi(value)
if err != nil || interval < 10 || interval > 43200 {
return fmt.Errorf("monitoring interval must be between 10 and 43200 seconds")
}
case "MonitorStoreDays", "GPUMonitorStoreDays", "VLLMMonitorStoreDays":
days, err := strconv.Atoi(value)
if err != nil || days < 1 {
return fmt.Errorf("monitoring retention must be a positive integer")
}
}
switch key {
case "MonitorStatus":
if value == constant.StatusEnable && global.MonitorCronID == 0 {
if value == constant.StatusEnable && monitorCancel == nil {
interval, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval"))
if err != nil {
return err
@@ -293,8 +321,9 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
return err
}
}
if value == constant.StatusDisable && global.MonitorCronID != 0 {
if value == constant.StatusDisable && monitorCancel != nil {
monitorCancel()
monitorCancel = nil
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
global.MonitorCronID = 0
}
@@ -303,30 +332,80 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
if err != nil {
return err
}
if status.Value == constant.StatusEnable && global.MonitorCronID != 0 {
if status.Value == constant.StatusEnable && monitorCancel != nil {
if err := StartMonitor(true, value); err != nil {
return err
}
}
case "GPUMonitorStatus":
if value == constant.StatusEnable && global.GPUMonitorCronID == 0 {
interval, err := settingRepo.GetValueByKey("GPUMonitorInterval")
if err != nil {
return err
}
if err := StartGPUMonitor(interval); err != nil {
return err
}
}
if value == constant.StatusDisable && global.GPUMonitorCronID != 0 {
global.Cron.Remove(global.GPUMonitorCronID)
global.GPUMonitorCronID = 0
}
case "GPUMonitorInterval":
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
if err != nil {
return err
}
if status == constant.StatusEnable {
if err := StartGPUMonitor(value); err != nil {
return err
}
}
case "VLLMMonitorStatus":
if value == constant.StatusEnable && global.VLLMMonitorCronID == 0 {
interval, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
if err != nil {
return err
}
if err := StartVLLMMonitor(interval); err != nil {
return err
}
}
if value == constant.StatusDisable && global.VLLMMonitorCronID != 0 {
global.Cron.Remove(global.VLLMMonitorCronID)
global.VLLMMonitorCronID = 0
}
case "VLLMMonitorInterval":
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
if err != nil {
return err
}
if status == constant.StatusEnable {
if err := StartVLLMMonitor(value); err != nil {
return err
}
}
}
return settingRepo.Update(key, value)
}
func (m *MonitorService) CleanData() error {
if err := global.MonitorDB.Exec("DELETE FROM monitor_bases").Error; err != nil {
return err
func (m *MonitorService) CleanData(monitorType string) error {
switch monitorType {
case "host":
return monitorRepo.CleanHost()
case "gpu":
gpuMonitorMutex.Lock()
defer gpuMonitorMutex.Unlock()
return monitorRepo.CleanGPU()
default:
return fmt.Errorf("unsupported monitoring cleanup type: %s", monitorType)
}
if err := global.MonitorDB.Exec("DELETE FROM monitor_ios").Error; err != nil {
return err
}
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() {
if m.ctx.Err() != nil {
return
}
var itemModel model.MonitorBase
totalPercent, _ := cpu.Percent(3*time.Second, false)
if len(totalPercent) == 1 {
@@ -362,7 +441,6 @@ func (m *MonitorService) Run() {
m.loadDiskIO()
m.loadNetIO()
m.saveGPUData()
MonitorStoreDays, err := settingRepo.Get(settingRepo.WithByKey("MonitorStoreDays"))
if err != nil {
@@ -373,30 +451,34 @@ func (m *MonitorService) Run() {
_ = monitorRepo.DelMonitorBase(timeForDelete)
_ = monitorRepo.DelMonitorIO(timeForDelete)
_ = monitorRepo.DelMonitorNet(timeForDelete)
_ = monitorRepo.DelMonitorGPU(timeForDelete)
}
func (m *MonitorService) loadDiskIO() {
ioStat, _ := disk.IOCounters()
ioStat, _ := disk.IOCountersWithContext(m.ctx)
var diskIOList []disk.IOCountersStat
for _, io := range ioStat {
diskIOList = append(diskIOList, io)
}
diskIOList = append(diskIOList, sumDiskIOCounters(ioStat))
m.DiskIO <- diskIOList
select {
case <-m.ctx.Done():
case m.DiskIO <- diskIOList:
}
}
func (m *MonitorService) loadNetIO() {
netStat, _ := net.IOCounters(true)
netStatAll, _ := net.IOCounters(false)
netStat, _ := net.IOCountersWithContext(m.ctx, true)
netStatAll, _ := net.IOCountersWithContext(m.ctx, false)
var netList []net.IOCountersStat
netList = append(netList, netStat...)
netList = append(netList, netStatAll...)
m.NetIO <- netList
select {
case <-m.ctx.Done():
case m.NetIO <- netList:
}
}
func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
defer close(m.DiskIO)
for {
select {
case <-ctx.Done():
@@ -446,14 +528,17 @@ func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
}
}
_ = monitorRepo.BatchCreateMonitorIO(ioList)
m.DiskIO <- ioStat2
select {
case <-ctx.Done():
return
case m.DiskIO <- ioStat2:
}
}
}
}
}
func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64) {
defer close(m.NetIO)
for {
select {
case <-ctx.Done():
@@ -483,7 +568,11 @@ func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64)
}
_ = monitorRepo.BatchCreateMonitorNet(netList)
m.NetIO <- netStat2
select {
case <-ctx.Done():
return
case m.NetIO <- netStat2:
}
}
}
}
@@ -592,27 +681,38 @@ func loadTopMem() []dto.Process {
}
func StartMonitor(removeBefore bool, interval string) error {
intervalItem, err := strconv.Atoi(interval)
if err != nil || intervalItem < 10 || intervalItem > 43200 {
return fmt.Errorf("invalid host monitoring interval: %s", interval)
}
if removeBefore {
monitorCancel()
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
}
intervalItem, err := strconv.Atoi(interval)
if err != nil {
return err
}
service := NewIMonitorService()
ctx, cancel := context.WithCancel(context.Background())
monitorCancel = cancel
service := &MonitorService{
DiskIO: make(chan []disk.IOCountersStat, 2),
NetIO: make(chan []net.IOCountersStat, 2),
ctx: ctx,
}
now := time.Now()
nextMinute := now.Truncate(time.Minute).Add(time.Minute)
time.AfterFunc(time.Until(nextMinute), func() {
timer := time.AfterFunc(time.Until(nextMinute), func() {
monitorSettingMutex.Lock()
defer monitorSettingMutex.Unlock()
if ctx.Err() != nil {
return
}
monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %ss", interval), service)
if err != nil {
return
}
global.MonitorCronID = monitorID
})
monitorCancel = func() {
cancel()
timer.Stop()
}
service.Run()
@@ -669,15 +769,50 @@ func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
return data
}
func (m *MonitorService) saveGPUData() {
status, err := settingRepo.GetValueByKey("MonitorStatus")
func StartGPUMonitor(interval string) error {
seconds, err := strconv.Atoi(interval)
if err != nil || seconds < 10 || seconds > 43200 {
return fmt.Errorf("invalid GPU monitoring interval: %s", interval)
}
service := &MonitorService{}
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveGPUData))
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
if err != nil {
global.LOG.Errorf("load monitor status failed: %v", err)
return err
}
if global.GPUMonitorCronID != 0 {
global.Cron.Remove(global.GPUMonitorCronID)
}
global.GPUMonitorCronID = id
return nil
}
func (m *MonitorService) saveGPUData() {
if !gpuMonitorMutex.TryLock() {
return
}
defer gpuMonitorMutex.Unlock()
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
if err != nil {
global.LOG.Errorf("Load GPU monitoring status failed: %v", err)
return
}
if status != constant.StatusEnable {
return
}
retention, err := settingRepo.GetValueByKey("GPUMonitorStoreDays")
if err != nil {
global.LOG.Errorf("Load GPU monitoring retention failed: %v", err)
return
}
days, err := strconv.Atoi(retention)
if err != nil || days < 1 {
global.LOG.Errorf("Invalid GPU monitoring retention: %s", retention)
return
}
if err := monitorRepo.DelMonitorGPU(time.Now().AddDate(0, 0, -days)); err != nil {
global.LOG.Errorf("Clean GPU monitoring data failed: %v", err)
}
exist, client := accelerator.New()
if !exist {
return
@@ -691,7 +826,7 @@ func (m *MonitorService) saveGPUData() {
global.LOG.Warnf("load accelerator monitor data partially failed, err: %v", warning)
}
intervalSeconds := 0
if setting, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval")); err == nil {
if setting, err := settingRepo.Get(settingRepo.WithByKey("GPUMonitorInterval")); err == nil {
intervalSeconds, _ = strconv.Atoi(setting.Value)
}
list := make([]model.MonitorGPU, 0, len(snapshot.Devices))
+279
View File
@@ -0,0 +1,279 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/vllm"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/robfig/cron/v3"
"golang.org/x/sync/errgroup"
)
var vllmMonitorMutex sync.Mutex
var vllmMetricsClient = &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext, IdleConnTimeout: 30 * time.Second},
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
}
func (m *MonitorService) LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) {
data := dto.MonitorVLLMData{Points: []model.MonitorVLLM{}}
if !req.EndTime.After(req.StartTime) {
return data, fmt.Errorf("invalid vLLM history request")
}
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return data, err
}
if install.App.Key != "vllm" {
return data, fmt.Errorf("not a vLLM installation")
}
loc, err := time.LoadLocation(common.LoadTimeZoneByCmd())
if err != nil {
return data, err
}
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
data.SampleCount, err = vllmMonitorRepo.Count(req.AppInstallID, req.StartTime, req.EndTime)
if err != nil || data.SampleCount == 0 {
return data, err
}
if data.SampleCount > 1200 {
data.BucketSeconds = (req.EndTime.Unix() - req.StartTime.Unix() + 600) / 600
}
points, err := vllmMonitorRepo.History(req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation)
if err != nil {
return data, err
}
if data.BucketSeconds > 0 {
if req.Aggregation != "max" {
for i := range points {
if err := vllm.AggregateHistograms(points[i].HistogramSamples, &points[i].MonitorVLLM); err != nil {
return data, err
}
}
}
next := 0
for bucket := int64(0); bucket <= (req.EndTime.Unix()-req.StartTime.Unix())/data.BucketSeconds; bucket++ {
var point model.MonitorVLLM
if next < len(points) && points[next].Bucket == bucket {
point = points[next].MonitorVLLM
next++
}
point.AppInstallID = req.AppInstallID
point.CreatedAt = time.Unix(req.StartTime.Unix()+bucket*data.BucketSeconds, 0).In(loc)
if bucket == 0 {
point.CreatedAt = req.StartTime
}
data.Points = append(data.Points, point)
}
} else {
setting, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
if err != nil {
return data, err
}
intervalSeconds, err := strconv.Atoi(setting)
if err != nil || intervalSeconds <= 0 {
return data, fmt.Errorf("invalid vLLM monitoring interval: %s", setting)
}
interval := time.Duration(intervalSeconds) * time.Second
for i, point := range points {
if i > 0 && point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: points[i-1].CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
}
data.Points = append(data.Points, point.MonitorVLLM)
}
}
return data, nil
}
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error) {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return model.MonitorVLLM{}, err
}
if install.App.Key != "vllm" {
return model.MonitorVLLM{}, fmt.Errorf("not a vLLM installation")
}
previous, err := vllmMonitorRepo.Latest(req.AppInstallID)
if err != nil {
return model.MonitorVLLM{}, err
}
point, err := collectVLLMMetrics(ctx, install, previous)
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
}
return point, nil
}
func (m *MonitorService) CleanVLLMMonitor(req dto.MonitorVLLMClean) error {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return err
}
if install.App.Key != "vllm" {
return fmt.Errorf("not a vLLM installation")
}
vllmMonitorMutex.Lock()
defer vllmMonitorMutex.Unlock()
return vllmMonitorRepo.CleanTarget(req.AppInstallID)
}
func StartVLLMMonitor(interval string) error {
seconds, err := strconv.Atoi(interval)
if err != nil || seconds < 10 || seconds > 43200 {
return fmt.Errorf("invalid vLLM monitoring interval: %s", interval)
}
service := &MonitorService{}
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveVLLMData))
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
if err != nil {
return err
}
if global.VLLMMonitorCronID != 0 {
global.Cron.Remove(global.VLLMMonitorCronID)
}
global.VLLMMonitorCronID = id
return nil
}
func (m *MonitorService) saveVLLMData() {
if !vllmMonitorMutex.TryLock() {
return
}
defer vllmMonitorMutex.Unlock()
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
if err != nil {
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
return
}
if status != constant.StatusEnable {
return
}
retention, err := settingRepo.GetValueByKey("VLLMMonitorStoreDays")
if err != nil {
global.LOG.Errorf("Load vLLM monitoring retention failed: %v", err)
return
}
days, err := strconv.Atoi(retention)
if err != nil || days < 1 {
global.LOG.Errorf("Invalid vLLM monitoring retention: %s", retention)
return
}
if err := vllmMonitorRepo.DeleteBefore(time.Now().AddDate(0, 0, -days)); err != nil {
global.LOG.Errorf("Clean vLLM monitoring data failed: %v", err)
}
apps, err := appRepo.GetBy(appRepo.WithKey("vllm"))
if err != nil {
global.LOG.Errorf("Load vLLM apps for monitoring failed: %v", err)
return
}
if len(apps) == 0 {
return
}
ids := make([]uint, 0, len(apps))
for _, app := range apps {
ids = append(ids, app.ID)
}
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppIdsIn(ids))
if err != nil {
global.LOG.Errorf("Load vLLM instances for monitoring failed: %v", err)
return
}
var group errgroup.Group
group.SetLimit(4)
for _, install := range installs {
group.Go(func() error {
previous, err := vllmMonitorRepo.Latest(install.ID)
if err != nil {
return err
}
point, err := collectVLLMMetrics(context.Background(), install, previous)
if err != nil {
global.LOG.Debugf("Collect vLLM metrics for %s failed: %v", install.Name, err)
}
return vllmMonitorRepo.Create(&point)
})
}
if err := group.Wait(); err != nil {
global.LOG.Errorf("Save vLLM monitoring data failed: %v", err)
}
}
func collectVLLMMetrics(ctx context.Context, install model.AppInstall, previous model.MonitorVLLM) (model.MonitorVLLM, error) {
point := model.MonitorVLLM{AppInstallID: install.ID, CreatedAt: time.Now(), Status: "unavailable"}
if install.HttpPort <= 0 || install.HttpPort > 65535 {
return point, fmt.Errorf("invalid vLLM port")
}
var env map[string]interface{}
if err := json.Unmarshal([]byte(install.Env), &env); err != nil {
return point, err
}
host, _ := env[constant.HostIP].(string)
host = strings.Trim(host, "[]")
switch host {
case "", "0.0.0.0":
host = "127.0.0.1"
case "::":
host = "::1"
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+net.JoinHostPort(host, strconv.Itoa(install.HttpPort))+"/metrics", nil)
if err != nil {
return point, err
}
response, err := vllmMetricsClient.Do(req)
if err != nil {
return point, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return point, fmt.Errorf("vLLM metrics returned HTTP %d", response.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024+1))
if err != nil {
return point, err
}
if len(body) > 8*1024*1024 {
return point, fmt.Errorf("vLLM metrics response too large")
}
metrics, err := vllm.Parse(bytes.NewReader(body))
if err != nil {
return point, err
}
point.CreatedAt = time.Now()
raw, err := json.Marshal(metrics)
if err != nil {
return point, err
}
var before vllm.Metrics
elapsed := point.CreatedAt.Sub(previous.CreatedAt).Seconds()
if previous.Status == "ok" && elapsed > 0 {
if err := json.Unmarshal([]byte(previous.RawMetrics), &before); err != nil {
return point, err
}
}
calculated, err := vllm.Calculate(metrics, before, elapsed)
if err != nil {
return point, err
}
calculated.AppInstallID = install.ID
calculated.CreatedAt = point.CreatedAt
calculated.RawMetrics = string(raw)
calculated.Status = "ok"
return calculated, nil
}
+22
View File
@@ -38,6 +38,28 @@ func Run() {
}
}
var gpuStatus, gpuInterval model.Setting
if err := global.DB.Where("key = ?", "GPUMonitorStatus").First(&gpuStatus).Error; err != nil {
global.LOG.Errorf("Load GPU monitoring status failed: %v", err)
} else if gpuStatus.Value == constant.StatusEnable {
if err := global.DB.Where("key = ?", "GPUMonitorInterval").First(&gpuInterval).Error; err != nil {
global.LOG.Errorf("Load GPU monitoring interval failed: %v", err)
} else if err := service.StartGPUMonitor(gpuInterval.Value); err != nil {
global.LOG.Errorf("Start GPU monitoring failed: %v", err)
}
}
var vllmStatus, vllmInterval model.Setting
if err := global.DB.Where("key = ?", "VLLMMonitorStatus").First(&vllmStatus).Error; err != nil {
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
} else if vllmStatus.Value == constant.StatusEnable {
if err := global.DB.Where("key = ?", "VLLMMonitorInterval").First(&vllmInterval).Error; err != nil {
global.LOG.Errorf("Load vLLM monitoring interval failed: %v", err)
} else if err := service.StartVLLMMonitor(vllmInterval.Value); err != nil {
global.LOG.Errorf("Start vLLM monitoring failed: %v", err)
}
}
if _, err := global.Cron.AddJob("@daily", job.NewWebsiteJob()); err != nil {
global.LOG.Errorf("can not add website corn job: %s", err.Error())
}
+11 -8
View File
@@ -14,12 +14,13 @@ import (
)
var (
DB *gorm.DB
MonitorDB *gorm.DB
GPUMonitorDB *gorm.DB
TaskDB *gorm.DB
CoreDB *gorm.DB
AlertDB *gorm.DB
DB *gorm.DB
MonitorDB *gorm.DB
GPUMonitorDB *gorm.DB
VLLMMonitorDB *gorm.DB
TaskDB *gorm.DB
CoreDB *gorm.DB
AlertDB *gorm.DB
LOG *logrus.Logger
CONF ServerConfig
@@ -29,8 +30,10 @@ var (
Dir SystemDir
Cron *cron.Cron
MonitorCronID cron.EntryID
Cron *cron.Cron
MonitorCronID cron.EntryID
VLLMMonitorCronID cron.EntryID
GPUMonitorCronID cron.EntryID
IsMaster bool
+1
View File
@@ -13,6 +13,7 @@ func Init() {
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.VLLMMonitorDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "vllm_monitor.db"), "vllm_monitor")
global.AlertDB = common.LoadDBConnByPath(path.Join(global.Dir.DbDir, "alert.db"), "alert")
if _, err := os.Stat(path.Join(global.Dir.DbDir, "core.db")); err == nil {
+2
View File
@@ -112,6 +112,8 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddDockerPortGuardReadOnly,
migrations.MigrateFirewallPortWhitelistSources,
migrations.AddAcceleratorMetrics,
migrations.AddVLLMMonitor,
migrations.AddMonitorSettings,
}
}
+25
View File
@@ -1915,3 +1915,28 @@ var AddAcceleratorMetrics = &gormigrate.Migration{
return global.GPUMonitorDB.AutoMigrate(&model.MonitorGPU{})
},
}
var AddVLLMMonitor = &gormigrate.Migration{
ID: "20260928-vllm-monitor",
Migrate: func(tx *gorm.DB) error {
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
},
}
var AddMonitorSettings = &gormigrate.Migration{
ID: "20260929-monitor-settings",
Migrate: func(tx *gorm.DB) error {
for _, key := range []string{"MonitorStatus", "MonitorInterval", "MonitorStoreDays"} {
var setting model.Setting
if err := tx.Where("key = ?", key).First(&setting).Error; err != nil {
return err
}
for _, prefix := range []string{"GPU", "VLLM"} {
if err := tx.Where("key = ?", prefix+key).FirstOrCreate(&model.Setting{Key: prefix + key, Value: setting.Value}).Error; err != nil {
return err
}
}
}
return nil
},
}
+3
View File
@@ -27,6 +27,9 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.GET("/gpu/load", baseApi.LoadGpuInfo)
aiToolsRouter.POST("/gpu/search", baseApi.LoadGPUMonitor)
aiToolsRouter.GET("/gpu/options", baseApi.GetCPUOptions)
aiToolsRouter.POST("/vllm/monitor/search", baseApi.LoadVLLMMonitor)
aiToolsRouter.POST("/vllm/monitor/current", baseApi.LoadVLLMCurrent)
aiToolsRouter.POST("/vllm/monitor/clean", baseApi.CleanVLLMMonitor)
aiToolsRouter.POST("/mcp/search", baseApi.PageMcpServers)
aiToolsRouter.POST("/mcp/server/detail", baseApi.LoadMcpServerDetail)
@@ -23,14 +23,6 @@ type providerResult struct {
}
func New() (bool, Client) {
return newClient()
}
func NewAll() (bool, Client) {
return New()
}
func newClient() (bool, Client) {
client := Client{}
if available, gpuClient := gpu.New(); available {
client.providers = append(client.providers, gpuProvider{client: gpuClient})
+335
View File
@@ -0,0 +1,335 @@
package vllm
import (
"bufio"
"encoding/json"
"fmt"
"io"
"math"
"sort"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/utils/re"
)
type Metrics map[string]map[string]float64
type histogram struct {
Sum float64 `json:"sum"`
Count float64 `json:"count"`
Buckets map[string]float64 `json:"buckets,omitempty"`
}
func Parse(reader io.Reader) (Metrics, error) {
metrics := Metrics{}
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 4096), 1024*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "vllm:") {
continue
}
match := re.GetRegex(re.VLLMMetricSamplePattern).FindStringSubmatch(line)
if match == nil {
return nil, fmt.Errorf("invalid vLLM metric sample")
}
name := strings.TrimPrefix(match[1], "vllm:")
switch name {
case "num_requests_running", "num_requests_waiting", "kv_cache_usage_perc", "gpu_cache_usage_perc", "prompt_tokens_total", "generation_tokens_total", "request_success_total":
default:
base := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(name, "_sum"), "_count"), "_bucket")
if base == name {
continue
}
switch base {
case "time_to_first_token_seconds", "time_per_output_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds":
default:
continue
}
}
value, err := strconv.ParseFloat(match[3], 64)
if err != nil {
return nil, fmt.Errorf("invalid vLLM metric value: %w", err)
}
if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 {
continue
}
if metrics[name] == nil {
metrics[name] = map[string]float64{}
}
labels := re.GetRegex(re.VLLMMetricLabelPattern).FindAllStringSubmatch(match[2], -1)
parts := make([]string, 0, len(labels))
for _, label := range labels {
parts = append(parts, label[1]+"="+label[2])
}
sort.Strings(parts)
metrics[name]["{"+strings.Join(parts, ",")+"}"] = value
}
if err := scanner.Err(); err != nil {
return nil, err
}
if len(metrics) == 0 {
return nil, fmt.Errorf("no supported vLLM metrics")
}
return metrics, nil
}
func Calculate(current, previous Metrics, elapsed float64) (model.MonitorVLLM, error) {
result := model.MonitorVLLM{}
for name, target := range map[string]**float64{
"num_requests_running": &result.Running,
"num_requests_waiting": &result.Waiting,
} {
if len(current[name]) == 0 {
continue
}
value := 0.0
for _, sample := range current[name] {
value += sample
}
*target = &value
}
cache := current["kv_cache_usage_perc"]
if len(cache) == 0 {
cache = current["gpu_cache_usage_perc"]
}
if len(cache) > 0 {
value := 0.0
for _, sample := range cache {
value += sample
}
value = value / float64(len(cache)) * 100
result.CacheUsage = &value
}
if elapsed <= 0 {
return result, nil
}
for name, target := range map[string]**float64{
"prompt_tokens_total": &result.PromptThroughput,
"generation_tokens_total": &result.GenerationThroughput,
"request_success_total": &result.RequestThroughput,
} {
if value := counterDelta(current[name], previous[name]); value != nil {
rate := *value / elapsed
*target = &rate
}
}
histograms := map[string]histogram{}
for _, name := range []string{"time_to_first_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds"} {
source := name
if name == "inter_token_latency_seconds" && len(current[name+"_count"]) == 0 {
source = "time_per_output_token_seconds"
}
sum := counterDelta(current[source+"_sum"], previous[source+"_sum"])
count := counterDelta(current[source+"_count"], previous[source+"_count"])
if sum == nil || count == nil {
continue
}
histograms[name] = histogram{Sum: *sum, Count: *count, Buckets: bucketDeltas(current[source+"_bucket"], previous[source+"_bucket"], *count)}
}
applyHistograms(&result, histograms)
encoded, err := json.Marshal(histograms)
if err != nil {
return model.MonitorVLLM{}, err
}
result.HistogramDeltas = string(encoded)
return result, nil
}
func AggregateHistograms(encoded string, result *model.MonitorVLLM) error {
var samples []map[string]histogram
if err := json.Unmarshal([]byte(encoded), &samples); err != nil {
return err
}
merged := map[string]histogram{}
for _, sample := range samples {
for name, item := range sample {
if item.Count == 0 {
continue
}
total, exists := merged[name]
if !exists {
merged[name] = item
continue
}
total.Sum += item.Sum
total.Count += item.Count
if len(total.Buckets) != len(item.Buckets) {
total.Buckets = nil
}
for bound, count := range total.Buckets {
addition, ok := item.Buckets[bound]
if !ok {
total.Buckets = nil
break
}
total.Buckets[bound] = count + addition
}
merged[name] = total
}
}
applyHistograms(result, merged)
return nil
}
func applyHistograms(result *model.MonitorVLLM, histograms map[string]histogram) {
for _, item := range []struct {
name string
mean **float64
quantiles []**float64
}{
{"time_to_first_token_seconds", &result.TimeToFirstToken, []**float64{&result.TimeToFirstTokenP50, &result.TimeToFirstTokenP90, &result.TimeToFirstTokenP95, &result.TimeToFirstTokenP99}},
{"inter_token_latency_seconds", &result.TimePerOutputToken, []**float64{&result.TimePerOutputTokenP50, &result.TimePerOutputTokenP90, &result.TimePerOutputTokenP95, &result.TimePerOutputTokenP99}},
{"e2e_request_latency_seconds", &result.RequestLatency, []**float64{&result.RequestLatencyP50, &result.RequestLatencyP90, &result.RequestLatencyP95, &result.RequestLatencyP99}},
{"request_prefill_time_seconds", &result.PrefillTime, nil},
{"request_decode_time_seconds", &result.DecodeTime, nil},
} {
histogram, ok := histograms[item.name]
if !ok || histogram.Count <= 0 {
continue
}
mean := histogram.Sum / histogram.Count
*item.mean = &mean
if len(item.quantiles) == 0 {
continue
}
quantiles := histogramQuantiles(histogram.Buckets)
for i, target := range item.quantiles {
*target = quantiles[i]
}
}
}
func counterDelta(current, previous map[string]float64) *float64 {
if len(current) == 0 || len(current) != len(previous) {
return nil
}
value := 0.0
for label, sample := range current {
before, ok := previous[label]
if !ok || sample < before {
return nil
}
value += sample - before
}
if math.IsInf(value, 0) || math.IsNaN(value) {
return nil
}
return &value
}
func bucketDeltas(current, previous map[string]float64, count float64) map[string]float64 {
if len(current) == 0 || len(current) != len(previous) {
return nil
}
groups := map[string]map[string]float64{}
for labels, sample := range current {
before, ok := previous[labels]
if !ok || sample < before {
return nil
}
bound := ""
var identity []string
for _, label := range re.GetRegex(re.VLLMMetricLabelPattern).FindAllStringSubmatch(labels, -1) {
if label[1] == "le" {
raw, err := strconv.Unquote(label[2])
if err != nil {
return nil
}
upper, err := strconv.ParseFloat(raw, 64)
if err != nil || math.IsNaN(upper) || upper < 0 {
return nil
}
bound = strconv.FormatFloat(upper, 'g', -1, 64)
} else {
identity = append(identity, label[1]+"="+label[2])
}
}
if bound == "" {
return nil
}
group := strings.Join(identity, ",")
if groups[group] == nil {
groups[group] = map[string]float64{}
}
if _, exists := groups[group][bound]; exists {
return nil
}
groups[group][bound] = sample - before
}
result := map[string]float64{}
for _, buckets := range groups {
if _, ok := buckets["+Inf"]; !ok || len(buckets) < 2 {
return nil
}
if len(result) > 0 && len(result) != len(buckets) {
return nil
}
for bound := range result {
if _, ok := buckets[bound]; !ok {
return nil
}
}
bounds := make([]float64, 0, len(buckets))
for bound := range buckets {
value, _ := strconv.ParseFloat(bound, 64)
bounds = append(bounds, value)
}
sort.Float64s(bounds)
previousCount := 0.0
for _, bound := range bounds {
key := strconv.FormatFloat(bound, 'g', -1, 64)
value := buckets[key]
if value < previousCount {
if previousCount-value > 1e-12*(previousCount+value) {
return nil
}
value = previousCount
}
result[key] += value
previousCount = value
}
}
if math.Abs(result["+Inf"]-count) > 1e-12*(result["+Inf"]+count) {
return nil
}
return result
}
func histogramQuantiles(buckets map[string]float64) [4]*float64 {
var quantiles [4]*float64
if len(buckets) < 2 || buckets["+Inf"] <= 0 {
return quantiles
}
bounds := make([]float64, 0, len(buckets)-1)
for bound := range buckets {
value, err := strconv.ParseFloat(bound, 64)
if err != nil || math.IsNaN(value) || value < 0 {
return quantiles
}
if !math.IsInf(value, 1) {
bounds = append(bounds, value)
}
}
if len(bounds) == 0 {
return quantiles
}
sort.Float64s(bounds)
for i, q := range [...]float64{0.5, 0.9, 0.95, 0.99} {
rank := q * buckets["+Inf"]
lower, before := 0.0, 0.0
value := bounds[len(bounds)-1]
for _, upper := range bounds {
count := buckets[strconv.FormatFloat(upper, 'g', -1, 64)]
if count >= rank && count > before {
value = lower + (upper-lower)*(rank-before)/(count-before)
break
}
lower, before = upper, count
}
quantiles[i] = &value
}
return quantiles
}
+4
View File
@@ -6,6 +6,8 @@ import (
)
const (
VLLMMetricSamplePattern = `^([a-zA-Z_:][a-zA-Z0-9_:]*)(\{(?:[^"\\]|"(?:\\.|[^"\\])*")*\})?\s+(\S+)(?:\s+.*)?$`
VLLMMetricLabelPattern = `([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("(?:[^"\\]|\\.)*")`
NumberAlphaPattern = `(\d+)([A-Za-z]+)`
ComposeDisallowedCharsPattern = `[^a-z0-9_-]+`
ComposeNamePattern = `^[a-z0-9][a-z0-9_-]{0,255}$`
@@ -61,6 +63,8 @@ var regexMap = make(map[string]*regexp.Regexp)
func Init() {
patterns := []string{
VLLMMetricSamplePattern,
VLLMMetricLabelPattern,
NumberAlphaPattern,
ComposeDisallowedCharsPattern,
ComposeNamePattern,
+1
View File
@@ -117,6 +117,7 @@ export namespace AI {
endTime: Date;
}
export interface MonitorGPUOptions {
supported: boolean;
gpuType: string;
options: Array<string>;
chartHide: Array<ChartHide>;
+6
View File
@@ -65,6 +65,12 @@ export namespace Host {
}
export interface MonitorSetting {
gpuMonitorStatus: string;
gpuMonitorStoreDays: string;
gpuMonitorInterval: string;
vllmMonitorStatus: string;
vllmMonitorStoreDays: string;
vllmMonitorInterval: string;
defaultNetwork: string;
defaultIO: string;
monitorStatus: string;
+2 -2
View File
@@ -28,8 +28,8 @@ export const getIOOptions = (currentNode?: string) => {
currentNode ? { headers: { CurrentNode: currentNode } } : {},
);
};
export const cleanMonitors = () => {
return http.post(`/hosts/monitor/clean`, {});
export const cleanMonitors = (type: 'host' | 'gpu') => {
return http.post(`/hosts/monitor/clean`, { type });
};
export const loadMonitorSetting = (currentNode?: string) => {
return http.get<Host.MonitorSetting>(
@@ -229,7 +229,7 @@ function initChart() {
const option = {
title: [
{
left: 'center',
left: props.option.titleLeft ?? 'center',
text: props.option.title,
show: props.option.title,
textStyle: {
@@ -257,6 +257,7 @@ function initChart() {
itemWidth: 8,
icon: 'circle',
...props.option.legend,
selected: { ...props.option.legend?.selected, ...itemSelect?.selected },
textStyle: {
...itemSelect?.textStyle,
...props.option.legend?.textStyle,
+32 -1
View File
@@ -1507,6 +1507,34 @@ const message = {
ignoreEos: 'Ignore EOS',
extraHeaders: 'Extra Headers',
},
vllmMonitor: {
cleanHelper:
'Clear monitoring history only for this vLLM instance? Other instances, host and GPU monitoring are unaffected.',
overview: 'Overview',
latencyAnalysis: 'Latency analysis',
requests: 'Requests',
tokenThroughput: 'Token throughput',
running: 'Running requests',
waiting: 'Waiting requests',
cacheUsage: 'KV cache usage',
promptThroughput: 'Input token throughput',
generationThroughput: 'Output token throughput',
requestThroughput: 'Completed requests',
timeToFirstToken: 'Mean time to first token',
timePerOutputToken: 'Mean inter-token latency',
requestLatency: 'Mean request latency',
unavailable: 'Metrics are unavailable. Check that the instance is running and /metrics is accessible.',
loadFailed: 'Failed to load monitoring data. Please retry.',
helper: 'Manage collection, interval and retention in monitoring settings.',
cache: 'KV cache',
throughput: 'Request throughput',
phaseTime: 'Prefill and decode time',
prefillTime: 'Mean prefill time',
decodeTime: 'Mean decode time',
requestLatencyChart: 'End-to-end request latency',
timeToFirstTokenChart: 'Time to first token',
timePerOutputTokenChart: 'Inter-token latency',
},
gpu: {
memoryActivity: 'Memory read/write activity',
hotspotTemperature: 'Hotspot temperature',
@@ -2119,6 +2147,9 @@ const message = {
cleanLogscope: 'Log retention',
},
monitor: {
hostMonitor: 'Host Monitoring',
gpuMonitor: 'GPU Monitoring',
vllmMonitor: 'vLLM Monitoring',
globalFilter: 'Global Filter',
enableMonitor: 'Monitoring Status',
storeDays: 'Retention Days',
@@ -2127,7 +2158,7 @@ const message = {
defaultIO: 'Default Disk',
defaultIOHelper: 'Default disk option displayed in monitoring and overview interfaces',
cleanMonitor: 'Clear Monitoring Records',
cleanHelper: 'This operation will clear all monitoring records including GPU. Continue?',
cleanHelper: 'This operation will clear all history for {0}. Continue?',
avgLoad: 'Load average',
loadDetail: 'Load detail',
resourceUsage: 'Utilization',
+33 -1
View File
@@ -1516,6 +1516,35 @@ const message = {
ignoreEos: 'Ignorar EOS',
extraHeaders: 'Encabezados adicionales',
},
vllmMonitor: {
cleanHelper:
'¿Borrar solo el historial de monitoreo de esta instancia de vLLM? No afecta a otras instancias ni al monitoreo del host o de GPU.',
overview: 'Resumen',
latencyAnalysis: 'Análisis de latencia',
requests: 'Solicitudes',
tokenThroughput: 'Rendimiento de tokens',
running: 'Solicitudes en ejecución',
waiting: 'Solicitudes en espera',
cacheUsage: 'Uso de caché KV',
promptThroughput: 'Rendimiento de tokens de entrada',
generationThroughput: 'Rendimiento de tokens de salida',
requestThroughput: 'Solicitudes completadas',
timeToFirstToken: 'Latencia media del primer token',
timePerOutputToken: 'Latencia media entre tokens',
requestLatency: 'Latencia media de solicitud',
unavailable:
'Métricas no disponibles. Compruebe que la instancia esté en ejecución y que /metrics sea accesible.',
loadFailed: 'No se pudieron cargar los datos. Inténtelo de nuevo.',
helper: 'Configura la recopilación, el intervalo y la retención en los ajustes de monitorización.',
cache: 'Caché KV',
throughput: 'Solicitudes/s',
phaseTime: 'Tiempo de precarga y decodificación',
prefillTime: 'Tiempo medio de precarga',
decodeTime: 'Tiempo medio de decodificación',
requestLatencyChart: 'Latencia de solicitud de extremo a extremo',
timeToFirstTokenChart: 'Tiempo hasta el primer token',
timePerOutputTokenChart: 'Latencia entre tokens',
},
gpu: {
memoryActivity: 'Actividad de lectura/escritura de memoria',
hotspotTemperature: 'Temperatura del punto caliente',
@@ -2155,6 +2184,9 @@ const message = {
cleanLogscope: 'Retención de logs',
},
monitor: {
hostMonitor: 'Monitoreo del host',
gpuMonitor: 'Monitoreo de GPU',
vllmMonitor: 'Monitoreo de vLLM',
globalFilter: 'Filtro Global',
enableMonitor: 'Estado de Monitoreo',
storeDays: 'Días de Retención',
@@ -2164,7 +2196,7 @@ const message = {
defaultIO: 'Disco Predeterminado',
defaultIOHelper: 'Opción de disco predeterminada mostrada en las interfaces de monitoreo y resumen',
cleanMonitor: 'Limpiar Registros de Monitoreo',
cleanHelper: 'Esta operación borrará todos los registros de monitoreo, incluidos los de GPU. ¿Continuar?',
cleanHelper: 'Esta operación borrará todo el historial de {0}. ¿Continuar?',
avgLoad: 'Carga promedio',
loadDetail: 'Detalle de carga',
resourceUsage: 'Utilización',
+32 -1
View File
@@ -1492,6 +1492,34 @@ const message = {
ignoreEos: 'نادیده گرفتن EOS',
extraHeaders: 'هدرهای اضافی',
},
vllmMonitor: {
cleanHelper:
'فقط سابقه نظارت این نمونه vLLM پاک شود؟ سایر نمونه\u200cها و نظارت میزبان و GPU تغییری نمی\u200cکنند.',
overview: 'نمای کلی',
latencyAnalysis: 'تحلیل تأخیر',
requests: 'تعداد درخواست‌ها',
tokenThroughput: 'توان عملیاتی توکن',
running: 'درخواست‌های در حال اجرا',
waiting: 'درخواست‌های در انتظار',
cacheUsage: 'مصرف حافظه نهان KV',
promptThroughput: 'گذردهی توکن ورودی',
generationThroughput: 'گذردهی توکن خروجی',
requestThroughput: 'نرخ درخواست‌های تکمیل‌شده',
timeToFirstToken: 'میانگین تأخیر توکن اول',
timePerOutputToken: 'میانگین تأخیر بین توکن‌ها',
requestLatency: 'میانگین تأخیر درخواست',
unavailable: 'معیارها در دسترس نیستند. از اجرای نمونه و دسترسی به /metrics مطمئن شوید.',
loadFailed: 'بارگیری داده‌های پایش ناموفق بود. دوباره تلاش کنید.',
helper: 'فعال‌سازی جمع‌آوری، فاصله زمانی و مدت نگهداری را در تنظیمات پایش تغییر دهید.',
cache: 'حافظه نهان KV',
throughput: 'گذردهی درخواست',
phaseTime: 'زمان پیش‌پرکردن و رمزگشایی',
prefillTime: 'میانگین زمان پیش‌پرکردن',
decodeTime: 'میانگین زمان رمزگشایی',
requestLatencyChart: 'تأخیر سرتاسری درخواست',
timeToFirstTokenChart: 'زمان تا توکن اول',
timePerOutputTokenChart: 'تأخیر بین توکن‌ها',
},
gpu: {
memoryActivity: 'فعالیت خواندن/نوشتن حافظه',
hotspotTemperature: 'دمای نقطه داغ',
@@ -2103,6 +2131,9 @@ const message = {
cleanLogscope: 'نگهداری لاگ',
},
monitor: {
hostMonitor: 'نظارت میزبان',
gpuMonitor: 'نظارت GPU',
vllmMonitor: 'نظارت vLLM',
globalFilter: 'فیلتر جهانی',
enableMonitor: 'وضعیت نظارت',
storeDays: 'روزهای نگهداری',
@@ -2111,7 +2142,7 @@ const message = {
defaultIO: 'دیسک پیش‌فرض',
defaultIOHelper: 'گزینه دیسک پیش‌فرض که در رابط‌های نظارت و نمای کلی نمایش داده می‌شود',
cleanMonitor: 'پاک‌سازی رکوردهای نظارت',
cleanHelper: 'این عملیات همه رکوردهای نظارت از جمله GPU را پاک می‌کند. ادامه می‌دهید؟',
cleanHelper: 'این عملیات تمام سوابق {0} را پاک می‌کند. ادامه می‌دهید؟',
avgLoad: 'میانگین بار',
loadDetail: 'جزئیات بار',
resourceUsage: 'استفاده از منابع',
+32 -1
View File
@@ -1495,6 +1495,34 @@ const message = {
ignoreEos: 'EOS を無視',
extraHeaders: '追加ヘッダー',
},
vllmMonitor: {
cleanHelper:
'この vLLM インスタンスの監視履歴のみを削除しますか?他のインスタンス、ホスト、GPU の監視には影響しません。',
overview: '概要',
latencyAnalysis: 'レイテンシ分析',
requests: 'リクエスト数',
tokenThroughput: 'トークンスループット',
running: '実行中のリクエスト',
waiting: '待機中のリクエスト',
cacheUsage: 'KVキャッシュ使用率',
promptThroughput: '入力トークンスループット',
generationThroughput: '出力トークンスループット',
requestThroughput: '完了リクエスト数/秒',
timeToFirstToken: '平均初回トークン遅延',
timePerOutputToken: '平均トークン間遅延',
requestLatency: '平均リクエスト遅延',
unavailable: '指標を取得できません。インスタンスが稼働し、/metrics にアクセスできるか確認してください。',
loadFailed: '監視データの読み込みに失敗しました。再試行してください。',
helper: '収集の有効化、間隔、保存期間は監視設定で変更できます。',
cache: 'KVキャッシュ',
throughput: 'リクエストスループット',
phaseTime: 'プリフィルとデコードの所要時間',
prefillTime: '平均プリフィル時間',
decodeTime: '平均デコード時間',
requestLatencyChart: 'エンドツーエンドのリクエスト遅延',
timeToFirstTokenChart: '初回トークン遅延',
timePerOutputTokenChart: 'トークン間遅延',
},
gpu: {
memoryActivity: 'メモリ読み書き稼働率',
hotspotTemperature: 'ホットスポット温度',
@@ -2108,6 +2136,9 @@ const message = {
cleanLogscope: 'ログ保持',
},
monitor: {
hostMonitor: 'ホスト監視',
gpuMonitor: 'GPU 監視',
vllmMonitor: 'vLLM 監視',
globalFilter: 'グローバルフィルター',
enableMonitor: '監視ステータス',
storeDays: '保存日数',
@@ -2116,7 +2147,7 @@ const message = {
defaultIO: 'デフォルトディスク',
defaultIOHelper: '監視および概要インターフェースに表示されるデフォルトのディスクオプション',
cleanMonitor: '監視記録をクリア',
cleanHelper: 'この操作により、GPUを含むすべての監視記録がクリアされます。続行しますか?',
cleanHelper: 'この操作は {0} の履歴をすべて削除します。続行しますか?',
avgLoad: 'ロード平均',
loadDetail: '詳細を読み込みます',
resourceUsage: '利用',
+32 -1
View File
@@ -1481,6 +1481,34 @@ const message = {
ignoreEos: 'EOS 무시',
extraHeaders: '추가 헤더',
},
vllmMonitor: {
cleanHelper:
'현재 vLLM 인스턴스의 모니터링 기록만 삭제하시겠습니까? 다른 인스턴스, 호스트 및 GPU 모니터링에는 영향을 주지 않습니다.',
overview: '개요',
latencyAnalysis: '지연 시간 분석',
requests: '요청 수',
tokenThroughput: '토큰 처리량',
running: '실행 중인 요청',
waiting: '대기 중인 요청',
cacheUsage: 'KV 캐시 사용률',
promptThroughput: '입력 토큰 처리량',
generationThroughput: '출력 토큰 처리량',
requestThroughput: '완료 요청 속도',
timeToFirstToken: '평균 첫 토큰 지연',
timePerOutputToken: '평균 토큰 간 지연',
requestLatency: '평균 요청 지연',
unavailable: '지표를 수집할 수 없습니다. 인스턴스 실행 상태와 /metrics 접근 가능 여부를 확인하세요.',
loadFailed: '모니터링 데이터를 불러오지 못했습니다. 다시 시도하세요.',
helper: '수집 활성화, 간격 및 보존 기간은 모니터링 설정에서 조정합니다.',
cache: 'KV 캐시',
throughput: '요청 처리량',
phaseTime: '프리필 및 디코딩 시간',
prefillTime: '평균 프리필 시간',
decodeTime: '평균 디코딩 시간',
requestLatencyChart: '종단 간 요청 지연',
timeToFirstTokenChart: '첫 토큰 지연',
timePerOutputTokenChart: '토큰 간 지연',
},
gpu: {
memoryActivity: '메모리 읽기/쓰기 활성률',
hotspotTemperature: '핫스폿 온도',
@@ -2082,6 +2110,9 @@ const message = {
cleanLogscope: '로그 보관 기간',
},
monitor: {
hostMonitor: '호스트 모니터링',
gpuMonitor: 'GPU 모니터링',
vllmMonitor: 'vLLM 모니터링',
globalFilter: '전역 필터',
enableMonitor: '모니터링 상태',
storeDays: '보관 일수',
@@ -2090,7 +2121,7 @@ const message = {
defaultIO: '기본 디스크',
defaultIOHelper: '모니터링 및 개요 인터페이스에 표시되는 기본 디스크 옵션',
cleanMonitor: '모니터링 기록 지우기',
cleanHelper: '이 작업은 GPU를 포함한 모든 모니터링 기록을 지웁니다. 계속하시겠습니까?',
cleanHelper: '이 작업은 {0}의 모든 기록을 삭제합니다. 계속하시겠습니까?',
avgLoad: '평균 부하',
loadDetail: '부하 세부사항',
resourceUsage: '자원 사용률',
+31 -1
View File
@@ -1474,6 +1474,33 @@ const message = {
ignoreEos: 'ລະເວັ້ນ EOS',
extraHeaders: 'Headers ເພີ່ມເຕີມ',
},
vllmMonitor: {
cleanHelper: 'ລຶບສະເພາະປະຫວັດການຕິດຕາມຂອງ vLLM ນີ້ບໍ? ບໍ່ກະທົບຕໍ່ອິນສະແຕນອື່ນ, ໂຮສ ຫຼື GPU.',
overview: 'ພາບລວມ',
latencyAnalysis: 'ການວິເຄາະຄວາມລ່າຊ້າ',
requests: 'ຈຳນວນຄຳຮ້ອງ',
tokenThroughput: 'ອັດຕາປະມວນຜົນໂທເຄັນ',
running: 'ຄຳຮ້ອງຂໍທີ່ກຳລັງເຮັດວຽກ',
waiting: 'ຄຳຮ້ອງຂໍທີ່ກຳລັງລໍຖ້າ',
cacheUsage: 'ການໃຊ້ງານແຄດ KV',
promptThroughput: 'ອັດຕາ Token ຂາເຂົ້າ',
generationThroughput: 'ອັດຕາ Token ຂາອອກ',
requestThroughput: 'ອັດຕາຄຳຮ້ອງຂໍສຳເລັດ',
timeToFirstToken: 'ເວລາລໍຖ້າສະເລ່ຍຂອງ Token ທຳອິດ',
timePerOutputToken: 'ເວລາລໍຖ້າສະເລ່ຍລະຫວ່າງ Token',
requestLatency: 'ເວລາລໍຖ້າສະເລ່ຍຂອງຄຳຮ້ອງຂໍ',
unavailable: 'ບໍ່ສາມາດເກັບຕົວຊີ້ວັດໄດ້. ກວດສອບວ່າອິນສະແຕນເຮັດວຽກ ແລະ ເຂົ້າເຖິງ /metrics ໄດ້.',
loadFailed: 'ໂຫຼດຂໍ້ມູນຕິດຕາມບໍ່ສຳເລັດ. ກະລຸນາລອງອີກຄັ້ງ.',
helper: 'ປັບການເປີດໃຊ້ການເກັບຂໍ້ມູນ, ໄລຍະຫ່າງ ແລະ ຈຳນວນມື້ເກັບຮັກສາໃນການຕັ້ງຄ່າການຕິດຕາມ.',
cache: 'ແຄດ KV',
throughput: 'ຄຳຮ້ອງຂໍ/ວິນາທີ',
phaseTime: 'ເວລາເຕີມລ່ວງໜ້າ ແລະ ຖອດລະຫັດ',
prefillTime: 'ເວລາເຕີມລ່ວງໜ້າສະເລ່ຍ',
decodeTime: 'ເວລາຖອດລະຫັດສະເລ່ຍ',
requestLatencyChart: 'ເວລາລໍຖ້າຄຳຮ້ອງຂໍຕັ້ງແຕ່ຕົ້ນຈົນຈົບ',
timeToFirstTokenChart: 'ເວລາລໍຖ້າ Token ທຳອິດ',
timePerOutputTokenChart: 'ເວລາລໍຖ້າລະຫວ່າງ Token',
},
gpu: {
memoryActivity: 'ກິດຈະກຳອ່ານ/ຂຽນໜ່ວຍຄວາມຈຳ',
hotspotTemperature: 'ອຸນຫະພູມຈຸດຮ້ອນ',
@@ -2062,6 +2089,9 @@ const message = {
cleanLogscope: 'ການເກັບຮັກສາບັນທຶກ',
},
monitor: {
hostMonitor: 'ການຕິດຕາມໂຮສ',
gpuMonitor: 'ການຕິດຕາມ GPU',
vllmMonitor: 'ການຕິດຕາມ vLLM',
globalFilter: 'ຕົວກອງທົ່ວໂລກ',
enableMonitor: 'ສະຖານະການຕິດຕາມ',
storeDays: 'ຈຳນວນມື້ທີ່ເກັບໄວ້',
@@ -2070,7 +2100,7 @@ const message = {
defaultIO: 'Disk ເລີ່ມຕົ້ນ',
defaultIOHelper: 'Disk ທີ່ຈະສະແດງໃນໜ້າ monitor ແລະ ພາບລວມ',
cleanMonitor: 'ລ້າງບັນທຶກການຕິດຕາມ',
cleanHelper: 'ການດຳເນີນການນີ້ຈະລ້າງບັນທຶກການຕິດຕາມທັງໝົດລວມເຖິງ GPU. ຕ້ອງການຕໍ່ຫຼືບໍ່?',
cleanHelper: 'ການດຳເນີນການນີ້ຈະລຶບປະຫວັດທັງໝົດຂອງ {0}. ສືບຕໍ່ບໍ?',
avgLoad: 'ຄ່າ Load ສະເລ່ຍ',
loadDetail: 'ລາຍລະອຽດ Load',
resourceUsage: 'ການນຳໃຊ້',
+32 -1
View File
@@ -1521,6 +1521,34 @@ const message = {
ignoreEos: 'Abaikan EOS',
extraHeaders: 'Pengepala Tambahan',
},
vllmMonitor: {
cleanHelper:
'Padamkan sejarah pemantauan untuk instans vLLM ini sahaja? Instans lain serta pemantauan hos dan GPU tidak terjejas.',
overview: 'Gambaran keseluruhan',
latencyAnalysis: 'Analisis kependaman',
requests: 'Permintaan',
tokenThroughput: 'Daya pemprosesan token',
running: 'Permintaan berjalan',
waiting: 'Permintaan menunggu',
cacheUsage: 'Penggunaan cache KV',
promptThroughput: 'Daya pemprosesan token input',
generationThroughput: 'Daya pemprosesan token output',
requestThroughput: 'Kadar permintaan selesai',
timeToFirstToken: 'Purata kependaman token pertama',
timePerOutputToken: 'Purata kependaman antara token',
requestLatency: 'Purata kependaman permintaan',
unavailable: 'Metrik tidak tersedia. Pastikan tika sedang berjalan dan /metrics boleh dicapai.',
loadFailed: 'Gagal memuatkan data pemantauan. Sila cuba lagi.',
helper: 'Urus pengumpulan, sela dan tempoh penyimpanan dalam tetapan pemantauan.',
cache: 'Cache KV',
throughput: 'Permintaan/s',
phaseTime: 'Masa praisi dan penyahkodan',
prefillTime: 'Purata masa praisi',
decodeTime: 'Purata masa penyahkodan',
requestLatencyChart: 'Kependaman permintaan hujung ke hujung',
timeToFirstTokenChart: 'Masa ke token pertama',
timePerOutputTokenChart: 'Kependaman antara token',
},
gpu: {
memoryActivity: 'Aktiviti baca/tulis memori',
hotspotTemperature: 'Suhu titik panas',
@@ -2145,6 +2173,9 @@ const message = {
cleanLogscope: 'Tempoh simpan log',
},
monitor: {
hostMonitor: 'Pemantauan Hos',
gpuMonitor: 'Pemantauan GPU',
vllmMonitor: 'Pemantauan vLLM',
globalFilter: 'Penapis Global',
enableMonitor: 'Status Pemantauan',
storeDays: 'Hari Penyimpanan',
@@ -2154,7 +2185,7 @@ const message = {
defaultIO: 'Cakera Lalai',
defaultIOHelper: 'Pilihan cakera lalai yang dipaparkan dalam antara muka pemantauan dan gambaran keseluruhan',
cleanMonitor: 'Kosongkan Rekod Pemantauan',
cleanHelper: 'Operasi ini akan membersihkan semua rekod pemantauan termasuk GPU. Teruskan?',
cleanHelper: 'Operasi ini akan memadamkan semua sejarah untuk {0}. Teruskan?',
avgLoad: 'Purata beban',
loadDetail: 'Butiran beban',
resourceUsage: 'Penggunaan sumber',
+33 -1
View File
@@ -1517,6 +1517,35 @@ const message = {
ignoreEos: 'Ignorar EOS',
extraHeaders: 'Cabeçalhos extras',
},
vllmMonitor: {
cleanHelper:
'Limpar apenas o histórico de monitoramento desta instância do vLLM? Outras instâncias e o monitoramento do host e da GPU não serão afetados.',
overview: 'Visão geral',
latencyAnalysis: 'Análise de latência',
requests: 'Requisições',
tokenThroughput: 'Vazão de tokens',
running: 'Solicitações em execução',
waiting: 'Solicitações em espera',
cacheUsage: 'Uso do cache KV',
promptThroughput: 'Taxa de tokens de entrada',
generationThroughput: 'Taxa de tokens de saída',
requestThroughput: 'Solicitações concluídas',
timeToFirstToken: 'Latência média do primeiro token',
timePerOutputToken: 'Latência média entre tokens',
requestLatency: 'Latência média da solicitação',
unavailable:
'Métricas indisponíveis. Verifique se a instância está em execução e se /metrics está acessível.',
loadFailed: 'Falha ao carregar os dados. Tente novamente.',
helper: 'Ajuste a coleta, o intervalo e a retenção nas configurações de monitoramento.',
cache: 'Cache KV',
throughput: 'Solicitações/s',
phaseTime: 'Tempo de preenchimento e decodificação',
prefillTime: 'Tempo médio de preenchimento',
decodeTime: 'Tempo médio de decodificação',
requestLatencyChart: 'Latência da solicitação de ponta a ponta',
timeToFirstTokenChart: 'Tempo até o primeiro token',
timePerOutputTokenChart: 'Latência entre tokens',
},
gpu: {
memoryActivity: 'Atividade de leitura/escrita da memória',
hotspotTemperature: 'Temperatura do ponto quente',
@@ -2150,6 +2179,9 @@ const message = {
cleanLogscope: 'Retenção de logs',
},
monitor: {
hostMonitor: 'Monitoramento do host',
gpuMonitor: 'Monitoramento da GPU',
vllmMonitor: 'Monitoramento do vLLM',
globalFilter: 'Filtro Global',
enableMonitor: 'Status de Monitoramento',
storeDays: 'Dias de Retenção',
@@ -2158,7 +2190,7 @@ const message = {
defaultIO: 'Disco Padrão',
defaultIOHelper: 'Opção de disco padrão exibida nas interfaces de monitoramento e visão geral',
cleanMonitor: 'Limpar Registros de Monitoramento',
cleanHelper: 'Esta operação limpará todos os registros de monitoramento, incluindo GPU. Continuar?',
cleanHelper: 'Esta operação limpará todo o histórico de {0}. Continuar?',
avgLoad: 'Média de carga',
loadDetail: 'Detalhes da carga',
resourceUsage: 'Utilização',
+32 -1
View File
@@ -1510,6 +1510,34 @@ const message = {
ignoreEos: 'Игнорировать EOS',
extraHeaders: 'Дополнительные заголовки',
},
vllmMonitor: {
cleanHelper:
'Удалить историю мониторинга только этого экземпляра vLLM? Другие экземпляры, мониторинг хоста и GPU не будут затронуты.',
overview: 'Обзор',
latencyAnalysis: 'Анализ задержек',
requests: 'Количество запросов',
tokenThroughput: 'Скорость обработки токенов',
running: 'Выполняемые запросы',
waiting: 'Ожидающие запросы',
cacheUsage: 'Использование KV-кэша',
promptThroughput: 'Скорость входных токенов',
generationThroughput: 'Скорость выходных токенов',
requestThroughput: 'Завершённые запросы',
timeToFirstToken: 'Среднее время до первого токена',
timePerOutputToken: 'Средняя задержка между токенами',
requestLatency: 'Средняя задержка запроса',
unavailable: 'Метрики недоступны. Проверьте, что экземпляр запущен и /metrics доступен.',
loadFailed: 'Не удалось загрузить данные мониторинга. Повторите попытку.',
helper: 'Сбор, интервал и срок хранения настраиваются в параметрах мониторинга.',
cache: 'KV-кэш',
throughput: 'Запросов/с',
phaseTime: 'Время предзаполнения и декодирования',
prefillTime: 'Среднее время предзаполнения',
decodeTime: 'Среднее время декодирования',
requestLatencyChart: 'Сквозная задержка запроса',
timeToFirstTokenChart: 'Время до первого токена',
timePerOutputTokenChart: 'Задержка между токенами',
},
gpu: {
memoryActivity: 'Активность чтения/записи памяти',
hotspotTemperature: 'Температура горячей точки',
@@ -2135,6 +2163,9 @@ const message = {
cleanLogscope: 'Хранение логов',
},
monitor: {
hostMonitor: 'Мониторинг хоста',
gpuMonitor: 'Мониторинг GPU',
vllmMonitor: 'Мониторинг vLLM',
globalFilter: 'Глобальный Фильтр',
enableMonitor: 'Статус Мониторинга',
storeDays: 'Дни Хранения',
@@ -2143,7 +2174,7 @@ const message = {
defaultIO: 'Диск по Умолчанию',
defaultIOHelper: 'Опция диска по умолчанию, отображаемая в интерфейсах мониторинга и обзора',
cleanMonitor: 'Очистить Записи Мониторинга',
cleanHelper: 'Эта операция очистит все записи мониторинга, включая GPU. Продолжить?',
cleanHelper: 'Эта операция удалит всю историю для {0}. Продолжить?',
avgLoad: 'Средняя нагрузка',
loadDetail: 'Детали нагрузки',
resourceUsage: 'Использование',
+33 -1
View File
@@ -1517,6 +1517,35 @@ const message = {
ignoreEos: "EOS'u yok say",
extraHeaders: 'Ek Başlıklar',
},
vllmMonitor: {
cleanHelper:
'Yalnızca bu vLLM örneğinin izleme geçmişi temizlensin mi? Diğer örnekler, ana makine ve GPU izlemesi etkilenmez.',
overview: 'Genel bakış',
latencyAnalysis: 'Gecikme analizi',
requests: 'İstek sayısı',
tokenThroughput: 'Token işlem hızı',
running: 'Çalışan istekler',
waiting: 'Bekleyen istekler',
cacheUsage: 'KV önbellek kullanımı',
promptThroughput: 'Girdi token hızı',
generationThroughput: 'Çıktı token hızı',
requestThroughput: 'Tamamlanan istek hızı',
timeToFirstToken: 'Ortalama ilk token gecikmesi',
timePerOutputToken: 'Ortalama token arası gecikme',
requestLatency: 'Ortalama istek gecikmesi',
unavailable:
'Metrikler kullanılamıyor. Örneğin çalıştığını ve /metrics yolunun erişilebilir olduğunu kontrol edin.',
loadFailed: 'İzleme verileri yüklenemedi. Tekrar deneyin.',
helper: 'Toplama, aralık ve saklama süresini izleme ayarlarından yönetin.',
cache: 'KV önbelleği',
throughput: 'İstek hızı',
phaseTime: 'Ön doldurma ve çözümleme süresi',
prefillTime: 'Ortalama ön doldurma süresi',
decodeTime: 'Ortalama çözümleme süresi',
requestLatencyChart: 'Uçtan uca istek gecikmesi',
timeToFirstTokenChart: 'İlk token süresi',
timePerOutputTokenChart: 'Token arası gecikme',
},
gpu: {
memoryActivity: 'Bellek okuma/yazma etkinliği',
hotspotTemperature: 'Sıcak nokta sıcaklığı',
@@ -2143,6 +2172,9 @@ const message = {
cleanLogscope: 'Günlük saklama',
},
monitor: {
hostMonitor: 'Ana Makine İzleme',
gpuMonitor: 'GPU İzleme',
vllmMonitor: 'vLLM İzleme',
globalFilter: 'Genel Filtre',
enableMonitor: 'İzleme Durumu',
storeDays: 'Saklama Günleri',
@@ -2151,7 +2183,7 @@ const message = {
defaultIO: 'Varsayılan Disk',
defaultIOHelper: 'İzleme ve genel bakış arayüzlerinde görüntülenen varsayılan disk seçeneği',
cleanMonitor: 'İzleme Kayıtlarını Temizle',
cleanHelper: 'Bu işlem GPU dahil tüm izleme kayıtlarını temizleyecektir. Devam etmek istiyor musunuz?',
cleanHelper: 'Bu işlem {0} için tüm geçmişi silecektir. Devam edilsin mi?',
avgLoad: 'Ortalama yük',
loadDetail: 'Yük detayı',
resourceUsage: 'Kullanım',
+31 -1
View File
@@ -1428,6 +1428,33 @@ const message = {
ignoreEos: '忽略 EOS',
extraHeaders: '額外請求標頭',
},
vllmMonitor: {
cleanHelper: '僅清空目前 vLLM 實例的監控歷史,不影響其他實例、主機或 GPU 監控,是否繼續?',
overview: '概覽',
latencyAnalysis: '延遲分析',
requests: '請求數',
tokenThroughput: 'Token 吞吐量',
running: '執行中請求',
waiting: '等待中請求',
cacheUsage: 'KV Cache 使用率',
promptThroughput: '輸入 Token 吞吐',
generationThroughput: '輸出 Token 吞吐',
requestThroughput: '完成請求速率',
timeToFirstToken: '平均首 Token 延遲',
timePerOutputToken: '平均 Token 間延遲',
requestLatency: '平均請求延遲',
unavailable: '無法採集指標,請確認實例已啟動且 /metrics 可存取。',
loadFailed: '監控資料載入失敗,請重試。',
helper: '採集開關、頻率和儲存天數統一在監控設定中調整。',
cache: 'KV 快取',
throughput: '請求吞吐',
phaseTime: '預填充與解碼耗時',
prefillTime: '平均預填充耗時',
decodeTime: '平均解碼耗時',
requestLatencyChart: '端到端請求延遲',
timeToFirstTokenChart: '首 Token 延遲',
timePerOutputTokenChart: 'Token 間延遲',
},
gpu: {
memoryActivity: '顯示記憶體讀寫活躍率',
hotspotTemperature: '熱點溫度',
@@ -2001,6 +2028,9 @@ const message = {
cleanLogscope: '清理類型',
},
monitor: {
hostMonitor: '主機監控',
gpuMonitor: 'GPU 監控',
vllmMonitor: 'vLLM 監控',
globalFilter: '全域過濾',
enableMonitor: '監控狀態',
storeDays: '保存天數',
@@ -2009,7 +2039,7 @@ const message = {
defaultIO: '預設磁碟',
defaultIOHelper: '預設監控和概覽介面顯示的磁碟選項',
cleanMonitor: '清空監控記錄',
cleanHelper: '該操作將清空包括 GPU 在內的所有監控記錄,是否繼續?',
cleanHelper: '此操作將清空 {0} 的所有歷史記錄,是否繼續?',
avgLoad: '平均負載',
loadDetail: '負載詳情',
resourceUsage: '資源使用率',
+31 -1
View File
@@ -1441,6 +1441,33 @@ const message = {
ignoreEos: '忽略 EOS',
extraHeaders: '额外请求头',
},
vllmMonitor: {
cleanHelper: '仅清空当前 vLLM 实例的监控历史,不影响其他实例、主机或 GPU 监控,是否继续?',
overview: '概览',
latencyAnalysis: '延迟分析',
requests: '请求数',
tokenThroughput: 'Token 吞吐',
running: '运行中请求',
waiting: '等待中请求',
cacheUsage: 'KV Cache 使用率',
promptThroughput: '输入 Token 吞吐',
generationThroughput: '输出 Token 吞吐',
requestThroughput: '完成请求速率',
timeToFirstToken: '平均首 Token 延迟',
timePerOutputToken: '平均 Token 间延迟',
requestLatency: '平均请求延迟',
unavailable: '无法采集指标,请确认实例已启动且 /metrics 可访问。',
loadFailed: '监控数据加载失败,请重试。',
helper: '采集开关、频率和保存天数在监控设置的 vLLM 监控中独立调整。',
cache: 'KV 缓存',
throughput: '请求吞吐',
phaseTime: '预填充与解码耗时',
prefillTime: '平均预填充耗时',
decodeTime: '平均解码耗时',
requestLatencyChart: '端到端请求延迟',
timeToFirstTokenChart: '首 Token 延迟',
timePerOutputTokenChart: 'Token 间延迟',
},
gpu: {
memoryActivity: '显存读写活跃率',
hotspotTemperature: '热点温度',
@@ -2030,6 +2057,9 @@ const message = {
cleanLogscope: '清理类型',
},
monitor: {
hostMonitor: '主机监控',
gpuMonitor: 'GPU 监控',
vllmMonitor: 'vLLM 监控',
globalFilter: '全局筛选',
enableMonitor: '监控状态',
storeDays: '保存天数',
@@ -2038,7 +2068,7 @@ const message = {
defaultIO: '默认磁盘',
defaultIOHelper: '默认监控和概览界面显示的磁盘选项',
cleanMonitor: '清空监控记录',
cleanHelper: '该操作将清空包括 GPU 在内的所有监控记录,是否继续?',
cleanHelper: '该操作将清空 {0} 的所有历史记录,是否继续?',
avgLoad: '平均负载',
loadDetail: '负载详情',
+100 -59
View File
@@ -1,66 +1,96 @@
<template>
<div v-loading="loading">
<RouterMenu />
<el-card class="history-toolbar">
<div class="flex flex-wrap gap-3 items-center">
<el-date-picker
v-model="timeRangeGlobal"
type="datetimerange"
style="max-width: 100%; width: 360px; flex-grow: 0"
range-separator="-"
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
:clearable="false"
:size="isMobile ? 'small' : 'default'"
@change="changeTimeRange"
/>
<el-select style="max-width: 100%" class="p-w-300" v-model="selectedDevice" @change="search">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-radio-group v-if="history && history.bucketSeconds > 0" v-model="aggregation" @change="search">
<el-radio-button value="avg">{{ $t('aiTools.gpu.historyAverage') }}</el-radio-button>
<el-radio-button value="max">{{ $t('aiTools.gpu.historyPeak') }}</el-radio-button>
</el-radio-group>
<template v-if="supported">
<el-card class="history-toolbar">
<div class="flex flex-wrap gap-3 items-center">
<el-date-picker
v-model="timeRangeGlobal"
type="datetimerange"
style="max-width: 100%; width: 360px; flex-grow: 0"
range-separator="-"
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
:clearable="false"
:size="isMobile ? 'small' : 'default'"
@change="changeTimeRange"
/>
<el-select style="max-width: 100%" class="p-w-300" v-model="selectedDevice" @change="search">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-radio-group v-if="history && history.bucketSeconds > 0" v-model="aggregation" @change="search">
<el-radio-button value="avg">{{ $t('aiTools.gpu.historyAverage') }}</el-radio-button>
<el-radio-button value="max">{{ $t('aiTools.gpu.historyPeak') }}</el-radio-button>
</el-radio-group>
<TableRefresh @search="refresh" />
</div>
<div v-if="history?.sampleCount" class="history-summary">
{{
history.bucketSeconds > 0
? $t('aiTools.gpu.historyAggregated', {
seconds: history.bucketSeconds,
count: history.sampleCount,
})
: $t('aiTools.gpu.historyRaw', { count: history.sampleCount })
}}
</div>
</el-card>
<el-alert
v-if="currentDevice?.legacy"
:title="$t('aiTools.gpu.legacyHistory')"
type="info"
:closable="false"
show-icon
/>
<el-alert
v-if="failed"
:title="$t('aiTools.gpu.historyLoadFailed')"
type="error"
:closable="false"
show-icon
/>
<el-empty v-else-if="!loading && !history?.sampleCount" :description="$t('commons.msg.noneData')" />
<el-empty
v-else-if="history?.sampleCount && !charts.length"
:description="$t('aiTools.gpu.historyMetricEmpty')"
/>
<el-row v-else-if="history?.sampleCount" :gutter="12">
<el-col v-for="chart in charts" :key="chart.id" :xs="24" :md="12">
<el-card class="card-interval">
<template #header>{{ chart.title }}</template>
<v-charts :id="chart.id" height="320px" type="line" :option="chart.option" :dataZoom="true" />
</el-card>
</el-col>
</el-row>
</template>
<LayoutContent v-else-if="!loading" :title="$t('aiTools.gpu.gpu')" :divider="true">
<template #rightToolBar>
<TableRefresh @search="refresh" />
</div>
<div v-if="history?.sampleCount" class="history-summary">
{{
history.bucketSeconds > 0
? $t('aiTools.gpu.historyAggregated', {
seconds: history.bucketSeconds,
count: history.sampleCount,
})
: $t('aiTools.gpu.historyRaw', { count: history.sampleCount })
}}
</div>
</el-card>
<el-alert
v-if="currentDevice?.legacy"
:title="$t('aiTools.gpu.legacyHistory')"
type="info"
:closable="false"
show-icon
/>
<el-alert v-if="failed" :title="$t('aiTools.gpu.historyLoadFailed')" type="error" :closable="false" show-icon />
<el-empty v-else-if="!loading && !history?.sampleCount" :description="$t('commons.msg.noneData')" />
<el-empty
v-else-if="history?.sampleCount && !charts.length"
:description="$t('aiTools.gpu.historyMetricEmpty')"
/>
<el-row v-else-if="history?.sampleCount" :gutter="12">
<el-col v-for="chart in charts" :key="chart.id" :xs="24" :md="12">
<el-card class="card-interval">
<template #header>{{ chart.title }}</template>
<v-charts :id="chart.id" height="320px" type="line" :option="chart.option" :dataZoom="true" />
</el-card>
</el-col>
</el-row>
</template>
<template #main>
<el-alert
v-if="failed"
:title="$t('aiTools.gpu.historyLoadFailed')"
type="error"
:closable="false"
show-icon
/>
<div v-else class="app-warn">
<div class="flx-center">
<span>{{ $t('aiTools.gpu.gpuHelper') }}</span>
</div>
<div>
<img src="@/assets/images/no_app.svg" />
</div>
</div>
</template>
</LayoutContent>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { loadGPUMonitor, getGPUOptions } from '@/api/modules/ai';
import RouterMenu from '@/views/ai/gpu/index.vue';
import { shortcuts } from '@/utils/shortcuts';
@@ -71,6 +101,7 @@ import { useGlobalStore } from '@/composables/useGlobalStore';
const { isMobile } = useGlobalStore();
const loading = ref(false);
const failed = ref(false);
const supported = ref(false);
const options = ref<(AI.ChartHide & { value: string; label: string })[]>([]);
const selectedDevice = ref('');
const aggregation = ref<'avg' | 'max'>('avg');
@@ -273,7 +304,10 @@ const search = async () => {
history.value = undefined;
failed.value = false;
const device = currentDevice.value;
if (!device || !timeRangeGlobal.value) return;
if (!device || !timeRangeGlobal.value) {
loading.value = false;
return;
}
loading.value = true;
try {
const response = await loadGPUMonitor({
@@ -296,11 +330,15 @@ const changeTimeRange = () => {
void search();
};
const loadOptions = async () => {
const id = ++requestID;
history.value = undefined;
loading.value = true;
failed.value = false;
try {
const response = await getGPUOptions();
options.value = (response.data.chartHide || []).map((item) => ({
if (id !== requestID) return;
supported.value = response.data.supported;
options.value = (supported.value ? response.data.chartHide || [] : []).map((item) => ({
...item,
value: item.deviceID || `legacy:${item.productName}`,
label: `${item.productName} · ${item.deviceID || i18n.global.t('aiTools.gpu.legacyDevice')}`,
@@ -308,9 +346,9 @@ const loadOptions = async () => {
if (!currentDevice.value) selectedDevice.value = options.value[0]?.value || '';
await search();
} catch {
failed.value = true;
if (id === requestID) failed.value = true;
} finally {
loading.value = false;
if (id === requestID) loading.value = false;
}
};
const refresh = () => {
@@ -364,6 +402,9 @@ const loadProcessType = (val: string) => {
};
onMounted(loadOptions);
onBeforeUnmount(() => {
requestID++;
});
</script>
<style scoped lang="scss">
@@ -26,8 +26,10 @@ import { updateMonitorSetting } from '@/api/modules/host';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
key: 'MonitorStoreDays' | 'GPUMonitorStoreDays' | 'VLLMMonitorStoreDays';
monitorStoreDays: number;
}
const settingKey = ref<DialogProps['key']>('MonitorStoreDays');
const drawerVisible = ref();
const loading = ref();
@@ -38,6 +40,7 @@ const form = reactive({
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
settingKey.value = params.key;
form.monitorStoreDays = params.monitorStoreDays;
drawerVisible.value = true;
};
@@ -47,7 +50,7 @@ const onSave = async (formEl: FormInstance | undefined) => {
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
await updateMonitorSetting('MonitorStoreDays', form.monitorStoreDays + '')
await updateMonitorSetting(settingKey.value, form.monitorStoreDays + '')
.then(() => {
loading.value = false;
handleClose();
+138 -24
View File
@@ -2,17 +2,18 @@
<div>
<MonitorRouter />
<LayoutContent v-loading="loading" :title="$t('menu.settings', 2)" :divider="true">
<LayoutContent v-loading="loading" :title="$t('menu.settings', 2)">
<template #main>
<el-form :model="form" @submit.prevent ref="panelFormRef" label-position="left" label-width="160px">
<el-form :model="form" @submit.prevent label-position="left" label-width="160px">
<el-row>
<el-col :span="1"><br /></el-col>
<el-col :span="12">
<h3 class="mb-4">{{ $t('monitor.hostMonitor') }}</h3>
<el-form-item :label="$t('monitor.enableMonitor')" prop="monitorStatus">
<el-switch
v-permission
v-node-admin
@change="onSaveStatus"
@change="onSaveStatus('MonitorStatus', form.monitorStatus)"
v-model="form.monitorStatus"
active-value="Enable"
inactive-value="Disable"
@@ -21,7 +22,12 @@
<el-form-item :label="$t('monitor.storeDays')" prop="monitorStoreDays">
<el-input disabled v-model="form.monitorStoreDays">
<template #append>
<el-button v-permission v-node-admin @click="onChangeStoreDays" icon="Setting">
<el-button
v-permission
v-node-admin
@click="onChangeStoreDays('MonitorStoreDays', form.monitorStoreDays)"
icon="Setting"
>
{{ $t('commons.button.set') }}
</el-button>
</template>
@@ -30,7 +36,12 @@
<el-form-item :label="$t('monitor.interval')" prop="monitorIntervalItem">
<el-input disabled v-model="form.monitorIntervalItem">
<template #append>
<el-button v-permission v-node-admin @click="onChangeInterval" icon="Setting">
<el-button
v-permission
v-node-admin
@click="onChangeInterval('MonitorInterval', form.monitorInterval)"
icon="Setting"
>
{{ $t('commons.button.set') }}
</el-button>
</template>
@@ -57,10 +68,97 @@
<span class="input-help">{{ $t('monitor.defaultIOHelper') }}</span>
</el-form-item>
<el-form-item>
<el-button v-permission v-node-admin @click="onClean()" icon="Delete">
<el-button v-permission v-node-admin @click="onClean('host')" icon="Delete">
{{ $t('monitor.cleanMonitor') }}
</el-button>
</el-form-item>
<el-divider />
<h3 class="mb-4">{{ $t('monitor.gpuMonitor') }}</h3>
<el-form-item :label="$t('monitor.enableMonitor')" prop="gpuMonitorStatus">
<el-switch
v-permission
v-node-admin
@change="onSaveStatus('GPUMonitorStatus', form.gpuMonitorStatus)"
v-model="form.gpuMonitorStatus"
active-value="Enable"
inactive-value="Disable"
/>
</el-form-item>
<el-form-item :label="$t('monitor.storeDays')" prop="gpuMonitorStoreDays">
<el-input disabled v-model="form.gpuMonitorStoreDays">
<template #append>
<el-button
v-permission
v-node-admin
@click="onChangeStoreDays('GPUMonitorStoreDays', form.gpuMonitorStoreDays)"
icon="Setting"
>
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('monitor.interval')" prop="gpuMonitorIntervalItem">
<el-input disabled v-model="form.gpuMonitorIntervalItem">
<template #append>
<el-button
v-permission
v-node-admin
@click="onChangeInterval('GPUMonitorInterval', form.gpuMonitorInterval)"
icon="Setting"
>
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button v-permission v-node-admin @click="onClean('gpu')" icon="Delete">
{{ $t('monitor.cleanMonitor') }}
</el-button>
</el-form-item>
<el-divider />
<h3 class="mb-4">{{ $t('monitor.vllmMonitor') }}</h3>
<el-form-item :label="$t('monitor.enableMonitor')" prop="vllmMonitorStatus">
<el-switch
v-permission
v-node-admin
@change="onSaveStatus('VLLMMonitorStatus', form.vllmMonitorStatus)"
v-model="form.vllmMonitorStatus"
active-value="Enable"
inactive-value="Disable"
/>
</el-form-item>
<el-form-item :label="$t('monitor.storeDays')" prop="vllmMonitorStoreDays">
<el-input disabled v-model="form.vllmMonitorStoreDays">
<template #append>
<el-button
v-permission
v-node-admin
@click="
onChangeStoreDays('VLLMMonitorStoreDays', form.vllmMonitorStoreDays)
"
icon="Setting"
>
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('monitor.interval')" prop="vllmMonitorIntervalItem">
<el-input disabled v-model="form.vllmMonitorIntervalItem">
<template #append>
<el-button
v-permission
v-node-admin
@click="onChangeInterval('VLLMMonitorInterval', form.vllmMonitorInterval)"
icon="Setting"
>
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
@@ -76,7 +174,7 @@
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import { ElMessageBox, FormInstance } from 'element-plus';
import { ElMessageBox } from 'element-plus';
import { cleanMonitors, loadMonitorSetting, updateMonitorSetting } from '@/api/modules/host';
import MonitorRouter from '@/views/host/monitor/index.vue';
import Interval from '@/views/host/monitor/setting/interval/index.vue';
@@ -88,16 +186,21 @@ import { MsgSuccess } from '@/utils/message';
import { splitTimeFromSecond, transTimeUnit } from '@/utils/validate';
const loading = ref();
const form = reactive({
gpuMonitorStatus: 'Disable',
gpuMonitorStoreDays: 30,
gpuMonitorInterval: 300,
gpuMonitorIntervalItem: '',
vllmMonitorStatus: 'Disable',
vllmMonitorStoreDays: 30,
vllmMonitorInterval: 300,
vllmMonitorIntervalItem: '',
monitorStatus: 'Disable',
monitorStoreDays: 30,
monitorInterval: 300,
timeItem: 5,
timeUnit: 'm',
monitorIntervalItem: '',
defaultNetwork: '',
defaultIO: '',
});
const panelFormRef = ref<FormInstance>();
const intervalRef = ref();
const daysRef = ref();
@@ -106,12 +209,20 @@ const ioRef = ref();
const search = async () => {
const res = await loadMonitorSetting();
form.gpuMonitorStatus = res.data.gpuMonitorStatus;
form.gpuMonitorStoreDays = Number(res.data.gpuMonitorStoreDays);
form.gpuMonitorInterval = Number(res.data.gpuMonitorInterval);
const gpuInterval = splitTimeFromSecond(form.gpuMonitorInterval);
form.gpuMonitorIntervalItem = transTimeUnit(gpuInterval.timeItem + gpuInterval.timeUnit);
form.vllmMonitorStatus = res.data.vllmMonitorStatus;
form.vllmMonitorStoreDays = Number(res.data.vllmMonitorStoreDays);
form.vllmMonitorInterval = Number(res.data.vllmMonitorInterval);
const vllmInterval = splitTimeFromSecond(form.vllmMonitorInterval);
form.vllmMonitorIntervalItem = transTimeUnit(vllmInterval.timeItem + vllmInterval.timeUnit);
form.monitorStatus = res.data.monitorStatus;
form.monitorInterval = Number(res.data.monitorInterval);
let item = splitTimeFromSecond(form.monitorInterval);
form.timeItem = item.timeItem;
form.timeUnit = item.timeUnit;
form.monitorIntervalItem = transTimeUnit(form.timeItem + form.timeUnit);
const item = splitTimeFromSecond(form.monitorInterval);
form.monitorIntervalItem = transTimeUnit(item.timeItem + item.timeUnit);
form.monitorStoreDays = Number(res.data.monitorStoreDays);
form.defaultNetwork =
@@ -119,23 +230,25 @@ const search = async () => {
form.defaultIO = res.data.defaultIO === 'all' ? i18n.global.t('commons.table.all') : res.data.defaultIO;
};
const onSaveStatus = async () => {
const onSaveStatus = async (key: 'MonitorStatus' | 'GPUMonitorStatus' | 'VLLMMonitorStatus', value: string) => {
loading.value = true;
await updateMonitorSetting('MonitorStatus', form.monitorStatus)
await updateMonitorSetting(key, value)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
.catch(async () => {
loading.value = false;
await search();
});
};
const onChangeStoreDays = () => {
daysRef.value.acceptParams({ monitorStoreDays: form.monitorStoreDays });
const onChangeStoreDays = (key: 'MonitorStoreDays' | 'GPUMonitorStoreDays' | 'VLLMMonitorStoreDays', value: number) => {
daysRef.value.acceptParams({ key, monitorStoreDays: value });
};
const onChangeInterval = () => {
intervalRef.value.acceptParams({ timeItem: form.timeItem, timeUnit: form.timeUnit });
const onChangeInterval = (key: 'MonitorInterval' | 'GPUMonitorInterval' | 'VLLMMonitorInterval', value: number) => {
const interval = splitTimeFromSecond(value);
intervalRef.value.acceptParams({ key, timeItem: interval.timeItem, timeUnit: interval.timeUnit });
};
const onChangeNetwork = () => {
networkRef.value.acceptParams({ defaultNetwork: form.defaultNetwork });
@@ -144,14 +257,15 @@ const onChangeIO = () => {
ioRef.value.acceptParams({ defaultIO: form.defaultIO });
};
const onClean = async () => {
ElMessageBox.confirm(i18n.global.t('monitor.cleanHelper'), i18n.global.t('monitor.cleanMonitor'), {
const onClean = async (type: 'host' | 'gpu') => {
const name = i18n.global.t(type === 'host' ? 'monitor.hostMonitor' : 'monitor.gpuMonitor');
ElMessageBox.confirm(i18n.global.t('monitor.cleanHelper', [name]), i18n.global.t('monitor.cleanMonitor'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
await cleanMonitors()
await cleanMonitors(type)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
@@ -34,9 +34,11 @@ import { transferTimeToSecond } from '@/utils/validate';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
key: 'MonitorInterval' | 'GPUMonitorInterval' | 'VLLMMonitorInterval';
timeItem: number;
timeUnit: string;
}
const settingKey = ref<DialogProps['key']>('MonitorInterval');
const drawerVisible = ref();
const loading = ref();
@@ -60,8 +62,10 @@ const rules = reactive({
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
settingKey.value = params.key;
form.timeItem = params.timeItem;
form.timeUnit = params.timeUnit;
loadInterval();
drawerVisible.value = true;
};
@@ -74,7 +78,7 @@ const onSave = async (formEl: FormInstance | undefined) => {
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
await updateMonitorSetting('MonitorInterval', form.monitorInterval + '')
await updateMonitorSetting(settingKey.value, form.monitorInterval + '')
.then(() => {
loading.value = false;
handleClose();
@@ -296,6 +296,13 @@ const exactReplacements: Record<string, string> = {
OpsReportAutoExport: 'xpack.opsReport.page.autoExport',
ComplexityVerification: 'setting.complexity',
MFAStatus: 'setting.mfa',
GPUMonitorStatus: 'monitor.enableMonitor',
GPUMonitorStoreDays: 'monitor.storeDays',
GPUMonitorInterval: 'monitor.interval',
VLLMMonitorStatus: 'monitor.enableMonitor',
VLLMMonitorStoreDays: 'monitor.storeDays',
VLLMMonitorInterval: 'monitor.interval',
MonitorInterval: 'monitor.interval',
MonitorStatus: 'monitor.enableMonitor',
MonitorStoreDays: 'monitor.storeDays',
ApiInterfaceStatus: 'setting.apiInterface',