feat: Support Ollama model management (#7866)
Some checks failed
SonarCloud Scan / SonarCloud (push) Failing after -3s

This commit is contained in:
ssongliu 2025-02-13 15:28:29 +08:00 committed by GitHub
parent f233ef7069
commit af8eef4a91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 2076 additions and 27 deletions

View file

@ -0,0 +1,112 @@
package v1
import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/gpu/common"
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/xpu"
"github.com/gin-gonic/gin"
)
// @Tags AITools
// @Summary Create Ollama model
// @Accept json
// @Param request body dto.OllamaModelName true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /aitools/ollama/model [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加模型 [name]","formatEN":"add Ollama model [name]"}
func (b *BaseApi) CreateOllamaModel(c *gin.Context) {
var req dto.OllamaModelName
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := AIToolService.Create(req.Name); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags AITools
// @Summary Page Ollama models
// @Accept json
// @Param request body dto.SearchWithPage true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /aitools/ollama/model/search [post]
func (b *BaseApi) SearchOllamaModel(c *gin.Context) {
var req dto.SearchWithPage
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, list, err := AIToolService.Search(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
// @Tags AITools
// @Summary Delete Ollama model
// @Accept json
// @Param request body dto.OllamaModelName true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /aitool/ollama/model/del [post]
func (b *BaseApi) DeleteOllamaModel(c *gin.Context) {
var req dto.OllamaModelName
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := AIToolService.Delete(req.Name); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags AITools
// @Summary Load gpu / xpu info
// @Accept json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /aitool/gpu/load [get]
func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
ok, client := gpu.New()
if ok {
info, err := client.LoadGpuInfo()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, info)
return
}
xpuOK, xpuClient := xpu.New()
if xpuOK {
info, err := xpuClient.LoadGpuInfo()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, info)
return
}
helper.SuccessWithData(c, &common.GpuInfo{})
}

View file

@ -15,6 +15,8 @@ var (
appService = service.NewIAppService()
appInstallService = service.NewIAppInstalledService()
AIToolService = service.NewIAIToolService()
containerService = service.NewIContainerService()
composeTemplateService = service.NewIComposeTemplateService()
imageRepoService = service.NewIImageRepoService()

View file

@ -0,0 +1,11 @@
package dto
type OllamaModelInfo struct {
Name string `json:"name"`
Size string `json:"size"`
Modified string `json:"modified"`
}
type OllamaModelName struct {
Name string `json:"name"`
}

View file

@ -0,0 +1,166 @@
package service
import (
"fmt"
"io"
"os"
"os/exec"
"path"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
)
type AIToolService struct{}
type IAIToolService interface {
Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error)
Create(name string) error
Delete(name string) error
}
func NewIAIToolService() IAIToolService {
return &AIToolService{}
}
func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) {
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
if err != nil {
return 0, nil, err
}
stdout, err := cmd.Execf("docker exec %s ollama list", ollamaBaseInfo.ContainerName)
if err != nil {
return 0, nil, err
}
var list []dto.OllamaModelInfo
modelMaps := make(map[string]struct{})
lines := strings.Split(stdout, "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) < 5 {
continue
}
if parts[0] == "NAME" {
continue
}
modelMaps[parts[0]] = struct{}{}
list = append(list, dto.OllamaModelInfo{Name: parts[0], Size: parts[2] + " " + parts[3], Modified: strings.Join(parts[4:], " ")})
}
entries, _ := os.ReadDir(path.Join(global.CONF.System.DataDir, "log", "AITools"))
for _, item := range entries {
if _, ok := modelMaps[item.Name()]; ok {
continue
}
if _, ok := modelMaps[item.Name()+":latest"]; ok {
continue
}
list = append(list, dto.OllamaModelInfo{Name: item.Name(), Size: "-", Modified: "-"})
}
if len(req.Info) != 0 {
length, count := len(list), 0
for count < length {
if !strings.Contains(list[count].Name, req.Info) {
list = append(list[:count], list[(count+1):]...)
length--
} else {
count++
}
}
}
var records []dto.OllamaModelInfo
total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
records = make([]dto.OllamaModelInfo, 0)
} else {
if end >= total {
end = total
}
records = list[start:end]
}
return int64(total), records, err
}
func (u *AIToolService) Create(name string) error {
if cmd.CheckIllegal(name) {
return buserr.New(constant.ErrCmdIllegal)
}
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
if err != nil {
return err
}
fileName := strings.ReplaceAll(name, ":", "-")
logItem := path.Join(global.CONF.System.DataDir, "log", "AITools", fileName)
if _, err := os.Stat(path.Dir(logItem)); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(path.Dir(logItem), os.ModePerm); err != nil {
return err
}
}
file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return err
}
go func() {
defer file.Close()
cmd := exec.Command("docker", "exec", ollamaBaseInfo.ContainerName, "ollama", "run", name)
multiWriter := io.MultiWriter(os.Stdout, file)
cmd.Stdout = multiWriter
cmd.Stderr = multiWriter
if err := cmd.Run(); err != nil {
global.LOG.Errorf("ollama pull %s failed, err: %v", name, err)
_, _ = file.WriteString("ollama pull failed!")
return
}
global.LOG.Infof("ollama pull %s successful!", name)
_, _ = file.WriteString("ollama pull successful!")
}()
return nil
}
func (u *AIToolService) Delete(name string) error {
if cmd.CheckIllegal(name) {
return buserr.New(constant.ErrCmdIllegal)
}
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
if err != nil {
return err
}
stdout, err := cmd.Execf("docker exec %s ollama list", ollamaBaseInfo.ContainerName)
if err != nil {
return err
}
isExist := false
lines := strings.Split(stdout, "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) < 5 {
continue
}
if parts[0] == "NAME" {
continue
}
if parts[0] == name {
isExist = true
break
}
}
if isExist {
stdout, err := cmd.Execf("docker exec %s ollama rm %s", ollamaBaseInfo.ContainerName, name)
if err != nil {
return fmt.Errorf("handle ollama rm %s failed, stdout: %s, err: %v", name, stdout, err)
}
}
logItem := path.Join(global.CONF.System.DataDir, "log", "AITools", name)
_ = os.Remove(logItem)
logItem2 := path.Join(global.CONF.System.DataDir, "log", "AITools", strings.TrimSuffix(name, ":latest"))
if logItem2 != logItem {
_ = os.Remove(logItem2)
}
return nil
}

View file

@ -10,19 +10,19 @@ import (
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/shirou/gopsutil/v3/load"
"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/net"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/xpu"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/copier"
"github.com/1Panel-dev/1Panel/backend/utils/xpack"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/load"
"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/net"
)
type DashboardService struct{}
@ -341,7 +341,17 @@ func loadDiskInfo() []dto.DiskInfo {
}
func loadGPUInfo() []dto.GPUInfo {
list := xpack.LoadGpuInfo()
ok, client := gpu.New()
var list []interface{}
if ok {
info, err := client.LoadGpuInfo()
if err != nil || len(info.GPUs) == 0 {
return nil
}
for _, item := range info.GPUs {
list = append(list, item)
}
}
if len(list) == 0 {
return nil
}
@ -359,7 +369,17 @@ func loadGPUInfo() []dto.GPUInfo {
}
func loadXpuInfo() []dto.XPUInfo {
list := xpack.LoadXpuInfo()
var list []interface{}
ok, xpuClient := xpu.New()
if ok {
xpus, err := xpuClient.LoadDashData()
if err != nil || len(xpus) == 0 {
return nil
}
for _, item := range xpus {
list = append(list, item)
}
}
if len(list) == 0 {
return nil
}

View file

@ -479,6 +479,14 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
}
case "image-pull", "image-push", "image-build", "compose-create":
logFilePath = path.Join(global.CONF.System.TmpDir, fmt.Sprintf("docker_logs/%s", req.Name))
case "ollama-model":
fileName := strings.ReplaceAll(req.Name, ":", "-")
if _, err := os.Stat(fileName); err != nil {
if strings.HasSuffix(req.Name, ":latest") {
fileName = strings.TrimSuffix(req.Name, ":latest")
}
}
logFilePath = path.Join(global.CONF.System.DataDir, "log", "AITools", fileName)
}
lines, isEndOfFile, total, err := files.ReadFileByLine(logFilePath, req.Page, req.PageSize, req.Latest)

View file

@ -23,5 +23,6 @@ func commonGroups() []CommonRouter {
&RuntimeRouter{},
&ProcessRouter{},
&WebsiteCARouter{},
&AIToolsRouter{},
}
}

View file

@ -0,0 +1,23 @@
package router
import (
v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1"
"github.com/1Panel-dev/1Panel/backend/middleware"
"github.com/gin-gonic/gin"
)
type AIToolsRouter struct {
}
func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter := Router.Group("aitools")
aiToolsRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth()).Use(middleware.PasswordExpired())
baseApi := v1.ApiGroupApp.BaseApi
{
aiToolsRouter.POST("/ollama/model", baseApi.CreateOllamaModel)
aiToolsRouter.POST("/ollama/model/search", baseApi.SearchOllamaModel)
aiToolsRouter.POST("/ollama/model/del", baseApi.DeleteOllamaModel)
aiToolsRouter.GET("/gpu/load", baseApi.LoadGpuInfo)
}
}

View file

@ -0,0 +1,37 @@
package common
type GpuInfo struct {
CudaVersion string `json:"cudaVersion"`
DriverVersion string `json:"driverVersion"`
Type string `json:"type"`
GPUs []GPU `json:"gpu"`
}
type GPU struct {
Index uint `json:"index"`
ProductName string `json:"productName"`
PersistenceMode string `json:"persistenceMode"`
BusID string `json:"busID"`
DisplayActive string `json:"displayActive"`
ECC string `json:"ecc"`
FanSpeed string `json:"fanSpeed"`
Temperature string `json:"temperature"`
PerformanceState string `json:"performanceState"`
PowerDraw string `json:"powerDraw"`
MaxPowerLimit string `json:"maxPowerLimit"`
MemUsed string `json:"memUsed"`
MemTotal string `json:"memTotal"`
GPUUtil string `json:"gpuUtil"`
ComputeMode string `json:"computeMode"`
MigMode string `json:"migMode"`
Processes []Process `json:"processes"`
}
type Process struct {
Pid string `json:"pid"`
Type string `json:"type"`
ProcessName string `json:"processName"`
UsedMemory string `json:"usedMemory"`
}

View file

@ -0,0 +1,65 @@
package gpu
import (
"bytes"
_ "embed"
"encoding/xml"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/gpu/common"
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/gpu/schema_v12"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
)
type NvidiaSMI struct{}
func New() (bool, NvidiaSMI) {
return cmd.Which("nvidia-smi"), NvidiaSMI{}
}
func (n NvidiaSMI) LoadGpuInfo() (*common.GpuInfo, error) {
std, err := cmd.ExecWithTimeOut("nvidia-smi -q -x", 5*time.Second)
if err != nil {
return nil, fmt.Errorf("calling nvidia-smi failed, err: %v", std)
}
data := []byte(std)
schema := "v11"
buf := bytes.NewBuffer(data)
decoder := xml.NewDecoder(buf)
for {
token, err := decoder.Token()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("reading token failed: %w", err)
}
d, ok := token.(xml.Directive)
if !ok {
continue
}
directive := string(d)
if !strings.HasPrefix(directive, "DOCTYPE") {
continue
}
parts := strings.Split(directive, " ")
s := strings.Trim(parts[len(parts)-1], "\" ")
if strings.HasPrefix(s, "nvsmi_device_") && strings.HasSuffix(s, ".dtd") {
schema = strings.TrimSuffix(strings.TrimPrefix(s, "nvsmi_device_"), ".dtd")
} else {
global.LOG.Debugf("Cannot find schema version in %q", directive)
}
break
}
if schema != "v12" {
return &common.GpuInfo{}, nil
}
return schema_v12.Parse(data)
}

View file

@ -0,0 +1,58 @@
package schema_v12
import (
"encoding/xml"
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/gpu/common"
)
func Parse(buf []byte) (*common.GpuInfo, error) {
var (
s smi
info common.GpuInfo
)
if err := xml.Unmarshal(buf, &s); err != nil {
return nil, err
}
info.Type = "nvidia"
info.CudaVersion = s.CudaVersion
info.DriverVersion = s.DriverVersion
if len(s.Gpu) == 0 {
return &info, nil
}
for i := 0; i < len(s.Gpu); i++ {
var gpuItem common.GPU
gpuItem.Index = uint(i)
gpuItem.ProductName = s.Gpu[i].ProductName
gpuItem.PersistenceMode = s.Gpu[i].PersistenceMode
gpuItem.BusID = s.Gpu[i].ID
gpuItem.DisplayActive = s.Gpu[i].DisplayActive
gpuItem.ECC = s.Gpu[i].EccMode.CurrentEcc
if gpuItem.ECC == "Enabled" {
gpuItem.ECC = s.Gpu[i].EccErrors.Volatile.DramUncorrectable
}
gpuItem.FanSpeed = s.Gpu[i].FanSpeed
gpuItem.Temperature = s.Gpu[i].Temperature.GpuTemp
gpuItem.PerformanceState = s.Gpu[i].PerformanceState
gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.PowerDraw
gpuItem.MaxPowerLimit = s.Gpu[i].GpuPowerReadings.MaxPowerLimit
gpuItem.MemUsed = s.Gpu[i].FbMemoryUsage.Used
gpuItem.MemTotal = s.Gpu[i].FbMemoryUsage.Total
gpuItem.GPUUtil = s.Gpu[i].Utilization.GpuUtil
gpuItem.ComputeMode = s.Gpu[i].ComputeMode
gpuItem.MigMode = s.Gpu[i].MigMode.CurrentMig
for _, process := range s.Gpu[i].Processes.ProcessInfo {
gpuItem.Processes = append(gpuItem.Processes, common.Process{
Pid: process.Pid,
Type: process.Type,
ProcessName: process.ProcessName,
UsedMemory: process.UsedMemory,
})
}
info.GPUs = append(info.GPUs, gpuItem)
}
return &info, nil
}

View file

@ -0,0 +1,294 @@
package schema_v12
type smi struct {
AttachedGpus string `xml:"attached_gpus"`
CudaVersion string `xml:"cuda_version"`
DriverVersion string `xml:"driver_version"`
Gpu []struct {
ID string `xml:"id,attr"`
AccountedProcesses struct{} `xml:"accounted_processes"`
AccountingMode string `xml:"accounting_mode"`
AccountingModeBufferSize string `xml:"accounting_mode_buffer_size"`
AddressingMode string `xml:"addressing_mode"`
ApplicationsClocks struct {
GraphicsClock string `xml:"graphics_clock"`
MemClock string `xml:"mem_clock"`
} `xml:"applications_clocks"`
Bar1MemoryUsage struct {
Free string `xml:"free"`
Total string `xml:"total"`
Used string `xml:"used"`
} `xml:"bar1_memory_usage"`
BoardID string `xml:"board_id"`
BoardPartNumber string `xml:"board_part_number"`
CcProtectedMemoryUsage struct {
Free string `xml:"free"`
Total string `xml:"total"`
Used string `xml:"used"`
} `xml:"cc_protected_memory_usage"`
ClockPolicy struct {
AutoBoost string `xml:"auto_boost"`
AutoBoostDefault string `xml:"auto_boost_default"`
} `xml:"clock_policy"`
Clocks struct {
GraphicsClock string `xml:"graphics_clock"`
MemClock string `xml:"mem_clock"`
SmClock string `xml:"sm_clock"`
VideoClock string `xml:"video_clock"`
} `xml:"clocks"`
ClocksEventReasons struct {
ClocksEventReasonApplicationsClocksSetting string `xml:"clocks_event_reason_applications_clocks_setting"`
ClocksEventReasonDisplayClocksSetting string `xml:"clocks_event_reason_display_clocks_setting"`
ClocksEventReasonGpuIdle string `xml:"clocks_event_reason_gpu_idle"`
ClocksEventReasonHwPowerBrakeSlowdown string `xml:"clocks_event_reason_hw_power_brake_slowdown"`
ClocksEventReasonHwSlowdown string `xml:"clocks_event_reason_hw_slowdown"`
ClocksEventReasonHwThermalSlowdown string `xml:"clocks_event_reason_hw_thermal_slowdown"`
ClocksEventReasonSwPowerCap string `xml:"clocks_event_reason_sw_power_cap"`
ClocksEventReasonSwThermalSlowdown string `xml:"clocks_event_reason_sw_thermal_slowdown"`
ClocksEventReasonSyncBoost string `xml:"clocks_event_reason_sync_boost"`
} `xml:"clocks_event_reasons"`
ComputeMode string `xml:"compute_mode"`
DefaultApplicationsClocks struct {
GraphicsClock string `xml:"graphics_clock"`
MemClock string `xml:"mem_clock"`
} `xml:"default_applications_clocks"`
DeferredClocks struct {
MemClock string `xml:"mem_clock"`
} `xml:"deferred_clocks"`
DisplayActive string `xml:"display_active"`
DisplayMode string `xml:"display_mode"`
DriverModel struct {
CurrentDm string `xml:"current_dm"`
PendingDm string `xml:"pending_dm"`
} `xml:"driver_model"`
EccErrors struct {
Aggregate struct {
DramCorrectable string `xml:"dram_correctable"`
DramUncorrectable string `xml:"dram_uncorrectable"`
SramCorrectable string `xml:"sram_correctable"`
SramUncorrectable string `xml:"sram_uncorrectable"`
} `xml:"aggregate"`
Volatile struct {
DramCorrectable string `xml:"dram_correctable"`
DramUncorrectable string `xml:"dram_uncorrectable"`
SramCorrectable string `xml:"sram_correctable"`
SramUncorrectable string `xml:"sram_uncorrectable"`
} `xml:"volatile"`
} `xml:"ecc_errors"`
EccMode struct {
CurrentEcc string `xml:"current_ecc"`
PendingEcc string `xml:"pending_ecc"`
} `xml:"ecc_mode"`
EncoderStats struct {
AverageFps string `xml:"average_fps"`
AverageLatency string `xml:"average_latency"`
SessionCount string `xml:"session_count"`
} `xml:"encoder_stats"`
Fabric struct {
State string `xml:"state"`
Status string `xml:"status"`
} `xml:"fabric"`
FanSpeed string `xml:"fan_speed"`
FbMemoryUsage struct {
Free string `xml:"free"`
Reserved string `xml:"reserved"`
Total string `xml:"total"`
Used string `xml:"used"`
} `xml:"fb_memory_usage"`
FbcStats struct {
AverageFps string `xml:"average_fps"`
AverageLatency string `xml:"average_latency"`
SessionCount string `xml:"session_count"`
} `xml:"fbc_stats"`
GpuFruPartNumber string `xml:"gpu_fru_part_number"`
GpuModuleID string `xml:"gpu_module_id"`
GpuOperationMode struct {
CurrentGom string `xml:"current_gom"`
PendingGom string `xml:"pending_gom"`
} `xml:"gpu_operation_mode"`
GpuPartNumber string `xml:"gpu_part_number"`
GpuPowerReadings struct {
CurrentPowerLimit string `xml:"current_power_limit"`
DefaultPowerLimit string `xml:"default_power_limit"`
MaxPowerLimit string `xml:"max_power_limit"`
MinPowerLimit string `xml:"min_power_limit"`
PowerDraw string `xml:"power_draw"`
PowerState string `xml:"power_state"`
RequestedPowerLimit string `xml:"requested_power_limit"`
} `xml:"gpu_power_readings"`
GpuResetStatus struct {
DrainAndResetRecommended string `xml:"drain_and_reset_recommended"`
ResetRequired string `xml:"reset_required"`
} `xml:"gpu_reset_status"`
GpuVirtualizationMode struct {
HostVgpuMode string `xml:"host_vgpu_mode"`
VirtualizationMode string `xml:"virtualization_mode"`
} `xml:"gpu_virtualization_mode"`
GspFirmwareVersion string `xml:"gsp_firmware_version"`
Ibmnpu struct {
RelaxedOrderingMode string `xml:"relaxed_ordering_mode"`
} `xml:"ibmnpu"`
InforomVersion struct {
EccObject string `xml:"ecc_object"`
ImgVersion string `xml:"img_version"`
OemObject string `xml:"oem_object"`
PwrObject string `xml:"pwr_object"`
} `xml:"inforom_version"`
MaxClocks struct {
GraphicsClock string `xml:"graphics_clock"`
MemClock string `xml:"mem_clock"`
SmClock string `xml:"sm_clock"`
VideoClock string `xml:"video_clock"`
} `xml:"max_clocks"`
MaxCustomerBoostClocks struct {
GraphicsClock string `xml:"graphics_clock"`
} `xml:"max_customer_boost_clocks"`
MigDevices struct {
MigDevice []struct {
Index string `xml:"index"`
GpuInstanceID string `xml:"gpu_instance_id"`
ComputeInstanceID string `xml:"compute_instance_id"`
EccErrorCount struct {
Text string `xml:",chardata" json:"text"`
VolatileCount struct {
SramUncorrectable string `xml:"sram_uncorrectable"`
} `xml:"volatile_count" json:"volatile_count"`
} `xml:"ecc_error_count" json:"ecc_error_count"`
FbMemoryUsage struct {
Total string `xml:"total"`
Reserved string `xml:"reserved"`
Used string `xml:"used"`
Free string `xml:"free"`
} `xml:"fb_memory_usage" json:"fb_memory_usage"`
Bar1MemoryUsage struct {
Total string `xml:"total"`
Used string `xml:"used"`
Free string `xml:"free"`
} `xml:"bar1_memory_usage" json:"bar1_memory_usage"`
} `xml:"mig_device" json:"mig_device"`
} `xml:"mig_devices" json:"mig_devices"`
MigMode struct {
CurrentMig string `xml:"current_mig"`
PendingMig string `xml:"pending_mig"`
} `xml:"mig_mode"`
MinorNumber string `xml:"minor_number"`
ModulePowerReadings struct {
CurrentPowerLimit string `xml:"current_power_limit"`
DefaultPowerLimit string `xml:"default_power_limit"`
MaxPowerLimit string `xml:"max_power_limit"`
MinPowerLimit string `xml:"min_power_limit"`
PowerDraw string `xml:"power_draw"`
PowerState string `xml:"power_state"`
RequestedPowerLimit string `xml:"requested_power_limit"`
} `xml:"module_power_readings"`
MultigpuBoard string `xml:"multigpu_board"`
Pci struct {
AtomicCapsInbound string `xml:"atomic_caps_inbound"`
AtomicCapsOutbound string `xml:"atomic_caps_outbound"`
PciBridgeChip struct {
BridgeChipFw string `xml:"bridge_chip_fw"`
BridgeChipType string `xml:"bridge_chip_type"`
} `xml:"pci_bridge_chip"`
PciBus string `xml:"pci_bus"`
PciBusID string `xml:"pci_bus_id"`
PciDevice string `xml:"pci_device"`
PciDeviceID string `xml:"pci_device_id"`
PciDomain string `xml:"pci_domain"`
PciGpuLinkInfo struct {
LinkWidths struct {
CurrentLinkWidth string `xml:"current_link_width"`
MaxLinkWidth string `xml:"max_link_width"`
} `xml:"link_widths"`
PcieGen struct {
CurrentLinkGen string `xml:"current_link_gen"`
DeviceCurrentLinkGen string `xml:"device_current_link_gen"`
MaxDeviceLinkGen string `xml:"max_device_link_gen"`
MaxHostLinkGen string `xml:"max_host_link_gen"`
MaxLinkGen string `xml:"max_link_gen"`
} `xml:"pcie_gen"`
} `xml:"pci_gpu_link_info"`
PciSubSystemID string `xml:"pci_sub_system_id"`
ReplayCounter string `xml:"replay_counter"`
ReplayRolloverCounter string `xml:"replay_rollover_counter"`
RxUtil string `xml:"rx_util"`
TxUtil string `xml:"tx_util"`
} `xml:"pci"`
PerformanceState string `xml:"performance_state"`
PersistenceMode string `xml:"persistence_mode"`
PowerReadings struct {
PowerState string `xml:"power_state"`
PowerManagement string `xml:"power_management"`
PowerDraw string `xml:"power_draw"`
PowerLimit string `xml:"power_limit"`
DefaultPowerLimit string `xml:"default_power_limit"`
EnforcedPowerLimit string `xml:"enforced_power_limit"`
MinPowerLimit string `xml:"min_power_limit"`
MaxPowerLimit string `xml:"max_power_limit"`
} `xml:"power_readings"`
Processes struct {
ProcessInfo []struct {
Pid string `xml:"pid"`
Type string `xml:"type"`
ProcessName string `xml:"process_name"`
UsedMemory string `xml:"used_memory"`
} `xml:"process_info"`
} `xml:"processes"`
ProductArchitecture string `xml:"product_architecture"`
ProductBrand string `xml:"product_brand"`
ProductName string `xml:"product_name"`
RemappedRows struct {
// Manually added
Correctable string `xml:"remapped_row_corr"`
Uncorrectable string `xml:"remapped_row_unc"`
Pending string `xml:"remapped_row_pending"`
Failure string `xml:"remapped_row_failure"`
} `xml:"remapped_rows"`
RetiredPages struct {
DoubleBitRetirement struct {
RetiredCount string `xml:"retired_count"`
RetiredPagelist string `xml:"retired_pagelist"`
} `xml:"double_bit_retirement"`
MultipleSingleBitRetirement struct {
RetiredCount string `xml:"retired_count"`
RetiredPagelist string `xml:"retired_pagelist"`
} `xml:"multiple_single_bit_retirement"`
PendingBlacklist string `xml:"pending_blacklist"`
PendingRetirement string `xml:"pending_retirement"`
} `xml:"retired_pages"`
Serial string `xml:"serial"`
SupportedClocks struct {
SupportedMemClock []struct {
SupportedGraphicsClock []string `xml:"supported_graphics_clock"`
Value string `xml:"value"`
} `xml:"supported_mem_clock"`
} `xml:"supported_clocks"`
SupportedGpuTargetTemp struct {
GpuTargetTempMax string `xml:"gpu_target_temp_max"`
GpuTargetTempMin string `xml:"gpu_target_temp_min"`
} `xml:"supported_gpu_target_temp"`
Temperature struct {
GpuTargetTemperature string `xml:"gpu_target_temperature"`
GpuTemp string `xml:"gpu_temp"`
GpuTempMaxGpuThreshold string `xml:"gpu_temp_max_gpu_threshold"`
GpuTempMaxMemThreshold string `xml:"gpu_temp_max_mem_threshold"`
GpuTempMaxThreshold string `xml:"gpu_temp_max_threshold"`
GpuTempSlowThreshold string `xml:"gpu_temp_slow_threshold"`
GpuTempTlimit string `xml:"gpu_temp_tlimit"`
MemoryTemp string `xml:"memory_temp"`
} `xml:"temperature"`
Utilization struct {
DecoderUtil string `xml:"decoder_util"`
EncoderUtil string `xml:"encoder_util"`
GpuUtil string `xml:"gpu_util"`
JpegUtil string `xml:"jpeg_util"`
MemoryUtil string `xml:"memory_util"`
OfaUtil string `xml:"ofa_util"`
} `xml:"utilization"`
UUID string `xml:"uuid"`
VbiosVersion string `xml:"vbios_version"`
Voltage struct {
GraphicsVolt string `xml:"graphics_volt"`
} `xml:"voltage"`
} `xml:"gpu"`
Timestamp string `xml:"timestamp"`
}

View file

@ -0,0 +1,43 @@
package xpu
type DeviceUtilByProc struct {
DeviceID int `json:"device_id"`
MemSize float64 `json:"mem_size"`
ProcessID int `json:"process_id"`
ProcessName string `json:"process_name"`
SharedMemSize float64 `json:"shared_mem_size"`
}
type DeviceUtilByProcList struct {
DeviceUtilByProcList []DeviceUtilByProc `json:"device_util_by_proc_list"`
}
type Device struct {
DeviceFunctionType string `json:"device_function_type"`
DeviceID int `json:"device_id"`
DeviceName string `json:"device_name"`
DeviceType string `json:"device_type"`
DrmDevice string `json:"drm_device"`
PciBdfAddress string `json:"pci_bdf_address"`
PciDeviceID string `json:"pci_device_id"`
UUID string `json:"uuid"`
VendorName string `json:"vendor_name"`
MemoryPhysicalSizeByte string `json:"memory_physical_size_byte"`
MemoryFreeSizeByte string `json:"memory_free_size_byte"`
DriverVersion string `json:"driver_version"`
}
type DeviceInfo struct {
DeviceList []Device `json:"device_list"`
}
type DeviceLevelMetric struct {
MetricsType string `json:"metrics_type"`
Value float64 `json:"value"`
}
type DeviceStats struct {
DeviceID int `json:"device_id"`
DeviceLevel []DeviceLevelMetric `json:"device_level"`
}

View file

@ -0,0 +1,256 @@
package xpu
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
)
type XpuSMI struct{}
func New() (bool, XpuSMI) {
return cmd.Which("xpu-smi"), XpuSMI{}
}
func (x XpuSMI) loadDeviceData(device Device, wg *sync.WaitGroup, res *[]XPUSimpleInfo, mu *sync.Mutex) {
defer wg.Done()
var xpu XPUSimpleInfo
xpu.DeviceID = device.DeviceID
xpu.DeviceName = device.DeviceName
var xpuData, statsData string
var xpuErr, statsErr error
var wgCmd sync.WaitGroup
wgCmd.Add(2)
go func() {
defer wgCmd.Done()
xpuData, xpuErr = cmd.ExecWithTimeOut(fmt.Sprintf("xpu-smi discovery -d %d -j", device.DeviceID), 5*time.Second)
}()
go func() {
defer wgCmd.Done()
statsData, statsErr = cmd.ExecWithTimeOut(fmt.Sprintf("xpu-smi stats -d %d -j", device.DeviceID), 5*time.Second)
}()
wgCmd.Wait()
if xpuErr != nil {
global.LOG.Errorf("calling xpu-smi discovery failed for device %d, err: %v\n", device.DeviceID, xpuErr)
return
}
var info Device
if err := json.Unmarshal([]byte(xpuData), &info); err != nil {
global.LOG.Errorf("xpuData json unmarshal failed for device %d, err: %v\n", device.DeviceID, err)
return
}
bytes, err := strconv.ParseInt(info.MemoryPhysicalSizeByte, 10, 64)
if err != nil {
global.LOG.Errorf("Error parsing memory size for device %d, err: %v\n", device.DeviceID, err)
return
}
xpu.Memory = fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024))
if statsErr != nil {
global.LOG.Errorf("calling xpu-smi stats failed for device %d, err: %v\n", device.DeviceID, statsErr)
return
}
var stats DeviceStats
if err := json.Unmarshal([]byte(statsData), &stats); err != nil {
global.LOG.Errorf("statsData json unmarshal failed for device %d, err: %v\n", device.DeviceID, err)
return
}
for _, stat := range stats.DeviceLevel {
switch stat.MetricsType {
case "XPUM_STATS_POWER":
xpu.Power = fmt.Sprintf("%.1fW", stat.Value)
case "XPUM_STATS_GPU_CORE_TEMPERATURE":
xpu.Temperature = fmt.Sprintf("%.1f°C", stat.Value)
case "XPUM_STATS_MEMORY_USED":
xpu.MemoryUsed = fmt.Sprintf("%.1fMB", stat.Value)
case "XPUM_STATS_MEMORY_UTILIZATION":
xpu.MemoryUtil = fmt.Sprintf("%.1f%%", stat.Value)
}
}
mu.Lock()
*res = append(*res, xpu)
mu.Unlock()
}
func (x XpuSMI) LoadDashData() ([]XPUSimpleInfo, error) {
data, err := cmd.ExecWithTimeOut("xpu-smi discovery -j", 5*time.Second)
if err != nil {
return nil, fmt.Errorf("calling xpu-smi failed, err: %w", err)
}
var deviceInfo DeviceInfo
if err := json.Unmarshal([]byte(data), &deviceInfo); err != nil {
return nil, fmt.Errorf("deviceInfo json unmarshal failed, err: %w", err)
}
var res []XPUSimpleInfo
var wg sync.WaitGroup
var mu sync.Mutex
for _, device := range deviceInfo.DeviceList {
wg.Add(1)
go x.loadDeviceData(device, &wg, &res, &mu)
}
wg.Wait()
sort.Slice(res, func(i, j int) bool {
return res[i].DeviceID < res[j].DeviceID
})
return res, nil
}
func (x XpuSMI) LoadGpuInfo() (*XpuInfo, error) {
data, err := cmd.ExecWithTimeOut("xpu-smi discovery -j", 5*time.Second)
if err != nil {
return nil, fmt.Errorf("calling xpu-smi failed, err: %w", err)
}
var deviceInfo DeviceInfo
if err := json.Unmarshal([]byte(data), &deviceInfo); err != nil {
return nil, fmt.Errorf("deviceInfo json unmarshal failed, err: %w", err)
}
res := &XpuInfo{
Type: "xpu",
}
var wg sync.WaitGroup
var mu sync.Mutex
for _, device := range deviceInfo.DeviceList {
wg.Add(1)
go x.loadDeviceInfo(device, &wg, res, &mu)
}
wg.Wait()
processData, err := cmd.ExecWithTimeOut("xpu-smi ps -j", 5*time.Second)
if err != nil {
return nil, fmt.Errorf("calling xpu-smi ps failed, err: %w", err)
}
var psList DeviceUtilByProcList
if err := json.Unmarshal([]byte(processData), &psList); err != nil {
return nil, fmt.Errorf("processData json unmarshal failed, err: %w", err)
}
for _, ps := range psList.DeviceUtilByProcList {
process := Process{
PID: ps.ProcessID,
Command: ps.ProcessName,
}
if ps.SharedMemSize > 0 {
process.SHR = fmt.Sprintf("%.1f MB", ps.SharedMemSize/1024)
}
if ps.MemSize > 0 {
process.Memory = fmt.Sprintf("%.1f MB", ps.MemSize/1024)
}
for index, xpu := range res.Xpu {
if xpu.Basic.DeviceID == ps.DeviceID {
res.Xpu[index].Processes = append(res.Xpu[index].Processes, process)
}
}
}
return res, nil
}
func (x XpuSMI) loadDeviceInfo(device Device, wg *sync.WaitGroup, res *XpuInfo, mu *sync.Mutex) {
defer wg.Done()
xpu := Xpu{
Basic: Basic{
DeviceID: device.DeviceID,
DeviceName: device.DeviceName,
VendorName: device.VendorName,
PciBdfAddress: device.PciBdfAddress,
},
}
var xpuData, statsData string
var xpuErr, statsErr error
var wgCmd sync.WaitGroup
wgCmd.Add(2)
go func() {
defer wgCmd.Done()
xpuData, xpuErr = cmd.ExecWithTimeOut(fmt.Sprintf("xpu-smi discovery -d %d -j", device.DeviceID), 5*time.Second)
}()
go func() {
defer wgCmd.Done()
statsData, statsErr = cmd.ExecWithTimeOut(fmt.Sprintf("xpu-smi stats -d %d -j", device.DeviceID), 5*time.Second)
}()
wgCmd.Wait()
if xpuErr != nil {
global.LOG.Errorf("calling xpu-smi discovery failed for device %d, err: %v\n", device.DeviceID, xpuErr)
return
}
var info Device
if err := json.Unmarshal([]byte(xpuData), &info); err != nil {
global.LOG.Errorf("xpuData json unmarshal failed for device %d, err: %v\n", device.DeviceID, err)
return
}
res.DriverVersion = info.DriverVersion
xpu.Basic.DriverVersion = info.DriverVersion
bytes, err := strconv.ParseInt(info.MemoryPhysicalSizeByte, 10, 64)
if err != nil {
global.LOG.Errorf("Error parsing memory size for device %d, err: %v\n", device.DeviceID, err)
return
}
xpu.Basic.Memory = fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024))
xpu.Basic.FreeMemory = info.MemoryFreeSizeByte
if statsErr != nil {
global.LOG.Errorf("calling xpu-smi stats failed for device %d, err: %v\n", device.DeviceID, statsErr)
return
}
var stats DeviceStats
if err := json.Unmarshal([]byte(statsData), &stats); err != nil {
global.LOG.Errorf("statsData json unmarshal failed for device %d, err: %v\n", device.DeviceID, err)
return
}
for _, stat := range stats.DeviceLevel {
switch stat.MetricsType {
case "XPUM_STATS_POWER":
xpu.Stats.Power = fmt.Sprintf("%.1fW", stat.Value)
case "XPUM_STATS_GPU_FREQUENCY":
xpu.Stats.Frequency = fmt.Sprintf("%.1fMHz", stat.Value)
case "XPUM_STATS_GPU_CORE_TEMPERATURE":
xpu.Stats.Temperature = fmt.Sprintf("%.1f°C", stat.Value)
case "XPUM_STATS_MEMORY_USED":
xpu.Stats.MemoryUsed = fmt.Sprintf("%.1fMB", stat.Value)
case "XPUM_STATS_MEMORY_UTILIZATION":
xpu.Stats.MemoryUtil = fmt.Sprintf("%.1f%%", stat.Value)
}
}
mu.Lock()
res.Xpu = append(res.Xpu, xpu)
mu.Unlock()
}

View file

@ -0,0 +1,49 @@
package xpu
type XpuInfo struct {
Type string `json:"type"`
DriverVersion string `json:"driverVersion"`
Xpu []Xpu `json:"xpu"`
}
type Xpu struct {
Basic Basic `json:"basic"`
Stats Stats `json:"stats"`
Processes []Process `json:"processes"`
}
type Basic struct {
DeviceID int `json:"deviceID"`
DeviceName string `json:"deviceName"`
VendorName string `json:"vendorName"`
DriverVersion string `json:"driverVersion"`
Memory string `json:"memory"`
FreeMemory string `json:"freeMemory"`
PciBdfAddress string `json:"pciBdfAddress"`
}
type Stats struct {
Power string `json:"power"`
Frequency string `json:"frequency"`
Temperature string `json:"temperature"`
MemoryUsed string `json:"memoryUsed"`
MemoryUtil string `json:"memoryUtil"`
}
type Process struct {
PID int `json:"pid"`
Command string `json:"command"`
SHR string `json:"shr"`
Memory string `json:"memory"`
}
type XPUSimpleInfo struct {
DeviceID int `json:"deviceID"`
DeviceName string `json:"deviceName"`
Memory string `json:"memory"`
Temperature string `json:"temperature"`
MemoryUsed string `json:"memoryUsed"`
Power string `json:"power"`
MemoryUtil string `json:"memoryUtil"`
}

View file

@ -29,14 +29,6 @@ func LoadRequestTransport() *http.Transport {
}
}
func LoadGpuInfo() []interface{} {
return nil
}
func LoadXpuInfo() []interface{} {
return nil
}
func StartClam(startClam model.Clam, isUpdate bool) (int, error) {
return 0, buserr.New(constant.ErrXpackNotFound)
}

View file

@ -0,0 +1,82 @@
import { ReqPage } from '.';
export namespace AITool {
export interface OllamaModelInfo {
name: string;
size: string;
modified: string;
}
export interface OllamaModelSearch extends ReqPage {
info: string;
}
export interface Info {
cudaVersion: string;
driverVersion: string;
type: string;
gpu: GPU[];
}
export interface GPU {
index: number;
productName: string;
persistenceMode: string;
busID: string;
displayActive: string;
ecc: string;
fanSpeed: string;
temperature: string;
performanceState: string;
powerDraw: string;
maxPowerLimit: string;
memUsed: string;
memTotal: string;
gpuUtil: string;
computeMode: string;
migMode: string;
processes: Process[];
}
export interface Process {
pid: string;
type: string;
processName: string;
usedMemory: string;
}
export interface XpuInfo {
type: string;
driverVersion: string;
xpu: Xpu[];
}
interface Xpu {
basic: Basic;
stats: Stats;
processes: XpuProcess[];
}
interface Basic {
deviceID: number;
deviceName: string;
vendorName: string;
driverVersion: string;
memory: string;
freeMemory: string;
pciBdfAddress: string;
}
interface Stats {
power: string;
frequency: string;
temperature: string;
memoryUsed: string;
memoryUtil: string;
}
interface XpuProcess {
pid: number;
command: string;
shr: string;
memory: string;
}
}

View file

@ -0,0 +1,17 @@
import { AITool } from '@/api/interface/ai-tool';
import http from '@/api';
import { ResPage } from '../interface';
export const createOllamaModel = (name: string) => {
return http.post(`/aitools/ollama/model`, { name: name });
};
export const deleteOllamaModel = (name: string) => {
return http.post(`/aitools/ollama/model/del`, { name: name });
};
export const searchOllamaModel = (params: AITool.OllamaModelSearch) => {
return http.post<ResPage<AITool.OllamaModelInfo>>(`/aitools/ollama/model/search`, params);
};
export const loadGPUInfo = () => {
return http.get<any>(`/aitools/gpu/load`);
};

View file

@ -31,7 +31,7 @@
>
{{ $t('app.restart') }}
</el-button>
<el-divider direction="vertical" />
<el-divider v-if="!hideSetting" direction="vertical" />
<el-button
type="primary"
link
@ -43,6 +43,7 @@
</el-button>
<el-divider v-if="data.app === 'OpenResty'" direction="vertical" />
<el-button
v-if="!hideSetting"
type="primary"
@click="setting"
link
@ -124,6 +125,10 @@ const props = defineProps({
type: String,
default: '',
},
hideSetting: {
type: Boolean,
default: false,
},
});
let key = ref('');

View file

@ -72,6 +72,8 @@ const stopSignals = [
'image pull successful!',
'image push failed!',
'image push successful!',
'ollama pull failed!',
'ollama pull successful!',
];
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading']);
const tailLog = ref(false);
@ -173,9 +175,11 @@ const getContent = async (pre: boolean) => {
}
if (res.data.lines && res.data.lines.length > 0) {
res.data.lines = res.data.lines.map((line) =>
line.replace(/\\u(\w{4})/g, function (match, grp) {
return String.fromCharCode(parseInt(grp, 16));
}),
line
.replace(/\\u(\w{4})/g, function (match, grp) {
return String.fromCharCode(parseInt(grp, 16));
})
.replace(/\x1b\[[0-9;]*[A-Za-z?](?!\d)/g, ''),
);
const newLogs = res.data.lines;
if (newLogs.length === readReq.pageSize && readReq.page < res.data.total) {

View file

@ -323,6 +323,7 @@ const message = {
firewall: '防火墙',
ssl: '证书',
database: '数据库',
ai_tools: 'AI',
container: '容器',
cronjob: '计划任务',
host: '主机',
@ -574,6 +575,50 @@ const message = {
remoteConnHelper2: '非容器或外部连接使用此地址',
localIP: '本机 IP',
},
ai_tools: {
model: {
model: '模型',
create: '添加模型',
create_helper: '查找需要添加的模型',
},
gpu: {
gpu: 'GPU 监控',
base: '基础信息',
gpuHelper: '当前系统未检测到 NVIDIA-SMI或者XPU-SMI 指令请检查后重试',
driverVersion: '驱动版本',
cudaVersion: 'CUDA 版本',
process: '进程信息',
type: '类型',
typeG: '图形',
typeC: '计算',
typeCG: '计算+图形',
processName: '进程名称',
processMemoryUsage: '显存使用',
temperatureHelper: 'GPU 温度过高会导致 GPU 频率下降',
performanceStateHelper: ' P0 (最大性能) P12 (最小性能)',
busID: '总线地址',
persistenceMode: '持续模式',
enabled: '开启',
disabled: '关闭',
persistenceModeHelper: '持续模式能更加快速地响应任务但相应待机功耗也会增加',
displayActive: '显卡初始化',
displayActiveT: '是',
displayActiveF: '否',
ecc: '是否开启错误检查和纠正技术',
computeMode: '计算模式',
default: '默认',
exclusiveProcess: '进程排他',
exclusiveThread: '线程排他',
prohibited: '禁止',
defaultHelper: '默认: 进程可以并发执行',
exclusiveProcessHelper: '进程排他: 只有一个 CUDA 上下文可以使用 GPU, 但可以由多个线程共享',
exclusiveThreadHelper: '线程排他: 只有一个线程在 CUDA 上下文中可以使用 GPU',
prohibitedHelper: '禁止: 不允许进程同时执行',
migModeHelper: '用于创建 MIG 实例在用户层实现 GPU 的物理隔离',
migModeNA: '不支持',
shr: '共享显存',
},
},
container: {
create: '创建容器',
edit: '编辑容器',

View file

@ -0,0 +1,34 @@
import { Layout } from '@/routers/constant';
const databaseRouter = {
sort: 4,
path: '/ai-tools',
component: Layout,
redirect: '/ai-tools/model',
meta: {
icon: 'p-database',
title: 'menu.ai_tools',
},
children: [
{
path: '/ai-tools/model',
name: 'OllamaModel',
component: () => import('@/views/ai-tools/model/index.vue'),
meta: {
title: 'ai_tools.model.model',
requiresAuth: true,
},
},
{
path: '/ai-tools/gpu',
name: 'GPU',
component: () => import('@/views/ai-tools/gpu/index.vue'),
meta: {
title: 'ai_tools.gpu.gpu',
requiresAuth: true,
},
},
],
};
export default databaseRouter;

View file

@ -1,7 +1,7 @@
import { Layout } from '@/routers/constant';
const containerRouter = {
sort: 5,
sort: 6,
path: '/containers',
component: Layout,
redirect: '/containers/container',

View file

@ -1,7 +1,7 @@
import { Layout } from '@/routers/constant';
const cronRouter = {
sort: 8,
sort: 9,
path: '/cronjobs',
component: Layout,
redirect: '/cronjobs',

View file

@ -1,7 +1,7 @@
import { Layout } from '@/routers/constant';
const databaseRouter = {
sort: 4,
sort: 5,
path: '/databases',
component: Layout,
redirect: '/databases/mysql',

View file

@ -1,7 +1,7 @@
import { Layout } from '@/routers/constant';
const hostRouter = {
sort: 6,
sort: 7,
path: '/hosts',
component: Layout,
redirect: '/hosts/security',

View file

@ -1,7 +1,7 @@
import { Layout } from '@/routers/constant';
const logsRouter = {
sort: 8,
sort: 10,
path: '/logs',
component: Layout,
redirect: '/logs/operation',

View file

@ -1,7 +1,7 @@
import { Layout } from '@/routers/constant';
const settingRouter = {
sort: 10,
sort: 12,
path: '/settings',
component: Layout,
redirect: '/settings/panel',

View file

@ -1,7 +1,7 @@
import { Layout } from '@/routers/constant';
const toolboxRouter = {
sort: 7,
sort: 8,
path: '/toolbox',
component: Layout,
redirect: '/toolbox/supervisor',

View file

@ -0,0 +1,367 @@
<template>
<div>
<RouterButton
:buttons="[
{
label: $t('ai_tools.gpu.gpu'),
path: '/xpack/gpu',
},
]"
/>
<div v-if="gpuType == 'nvidia'">
<LayoutContent
v-loading="loading"
:title="$t('ai_tools.gpu.gpu')"
:divider="true"
v-if="gpuInfo.driverVersion.length !== 0 && !loading"
>
<template #toolbar>
<el-row>
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16" />
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
<TableSetting @search="refresh()" />
</el-col>
</el-row>
</template>
<template #main>
<el-descriptions direction="vertical" :column="14" border>
<el-descriptions-item :label="$t('ai_tools.gpu.driverVersion')" width="50%" :span="7">
{{ gpuInfo.driverVersion }}
</el-descriptions-item>
<el-descriptions-item :label="$t('ai_tools.gpu.cudaVersion')" :span="7">
{{ gpuInfo.cudaVersion }}
</el-descriptions-item>
</el-descriptions>
<el-collapse v-model="activeNames" class="mt-5">
<el-collapse-item v-for="item in gpuInfo.gpu" :key="item.index" :name="item.index">
<template #title>
<span class="name-class">{{ item.index + '. ' + item.productName }}</span>
</template>
<span class="title-class">{{ $t('ai_tools.gpu.base') }}</span>
<el-descriptions direction="vertical" :column="6" border size="small" class="mt-2">
<el-descriptions-item :label="$t('monitor.gpuUtil')">
{{ item.gpuUtil }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
{{ $t('monitor.temperature') }}
<el-tooltip placement="top" :content="$t('ai_tools.gpu.temperatureHelper')">
<el-icon class="icon-item"><InfoFilled /></el-icon>
</el-tooltip>
</div>
</template>
{{ item.temperature.replaceAll('C', '°C') }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
{{ $t('monitor.performanceState') }}
<el-tooltip
placement="top"
:content="$t('ai_tools.gpu.performanceStateHelper')"
>
<el-icon class="icon-item"><InfoFilled /></el-icon>
</el-tooltip>
</div>
</template>
{{ item.performanceState }}
</el-descriptions-item>
<el-descriptions-item :label="$t('monitor.powerUsage')">
{{ item.powerDraw }} / {{ item.maxPowerLimit }}
</el-descriptions-item>
<el-descriptions-item :label="$t('monitor.memoryUsage')">
{{ item.memUsed }} / {{ item.memTotal }}
</el-descriptions-item>
<el-descriptions-item :label="$t('monitor.fanSpeed')">
{{ item.fanSpeed }}
</el-descriptions-item>
<el-descriptions-item :label="$t('ai_tools.gpu.busID')">
{{ item.busID }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
{{ $t('ai_tools.gpu.persistenceMode') }}
<el-tooltip
placement="top"
:content="$t('ai_tools.gpu.persistenceModeHelper')"
>
<el-icon class="icon-item"><InfoFilled /></el-icon>
</el-tooltip>
</div>
</template>
{{ $t('ai_tools.gpu.' + item.persistenceMode.toLowerCase()) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('ai_tools.gpu.displayActive')">
{{
lowerCase(item.displayActive) === 'disabled'
? $t('ai_tools.gpu.displayActiveF')
: $t('ai_tools.gpu.displayActiveT')
}}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
Uncorr. ECC
<el-tooltip placement="top" :content="$t('ai_tools.gpu.ecc')">
<el-icon class="icon-item"><InfoFilled /></el-icon>
</el-tooltip>
</div>
</template>
{{ loadEcc(item.ecc) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('ai_tools.gpu.computeMode')">
<template #label>
<div class="cell-item">
{{ $t('ai_tools.gpu.computeMode') }}
<el-tooltip placement="top">
<template #content>
{{ $t('ai_tools.gpu.defaultHelper') }}
<br />
{{ $t('ai_tools.gpu.exclusiveProcessHelper') }}
<br />
{{ $t('ai_tools.gpu.exclusiveThreadHelper') }}
<br />
{{ $t('ai_tools.gpu.prohibitedHelper') }}
</template>
<el-icon class="icon-item"><InfoFilled /></el-icon>
</el-tooltip>
</div>
</template>
{{ loadComputeMode(item.computeMode) }}
</el-descriptions-item>
<el-descriptions-item label="MIG.M">
<template #label>
<div class="cell-item">
MIG M.
<el-tooltip placement="top">
<template #content>
{{ $t('ai_tools.gpu.migModeHelper') }}
</template>
<el-icon class="icon-item"><InfoFilled /></el-icon>
</el-tooltip>
</div>
</template>
{{
item.migMode === 'N/A'
? $t('ai_tools.gpu.migModeNA')
: $t('ai_tools.gpu.' + lowerCase(item.migMode))
}}
</el-descriptions-item>
</el-descriptions>
<div class="mt-5">
<span class="title-class">{{ $t('ai_tools.gpu.process') }}</span>
</div>
<el-table :data="item.processes" v-if="item.processes?.length !== 0">
<el-table-column label="PID" prop="pid" />
<el-table-column :label="$t('ai_tools.gpu.type')" prop="type">
<template #default="{ row }">
{{ loadProcessType(row.type) }}
</template>
</el-table-column>
<el-table-column :label="$t('ai_tools.gpu.processName')" prop="processName" />
<el-table-column :label="$t('ai_tools.gpu.processMemoryUsage')" prop="usedMemory" />
</el-table>
</el-collapse-item>
</el-collapse>
</template>
</LayoutContent>
</div>
<div v-else>
<LayoutContent
v-loading="loading"
:title="$t('ai_tools.gpu.gpu')"
:divider="true"
v-if="xpuInfo.driverVersion.length !== 0 && !loading"
>
<template #toolbar>
<el-row>
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16" />
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
<TableSetting @search="refresh()" />
</el-col>
</el-row>
</template>
<template #main>
<el-descriptions direction="vertical" :column="14" border>
<el-descriptions-item :label="$t('ai_tools.gpu.driverVersion')" width="50%" :span="7">
{{ xpuInfo.driverVersion }}
</el-descriptions-item>
</el-descriptions>
<el-collapse v-model="activeNames" class="mt-5">
<el-collapse-item
v-for="item in xpuInfo.xpu"
:key="item.basic.deviceID"
:name="item.basic.deviceID"
>
<template #title>
<span class="name-class">{{ item.basic.deviceID + '. ' + item.basic.deviceName }}</span>
</template>
<span class="title-class">{{ $t('ai_tools.gpu.base') }}</span>
<el-descriptions direction="vertical" :column="6" border size="small" class="mt-2">
<el-descriptions-item :label="$t('monitor.gpuUtil')">
{{ item.stats.memoryUtil }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
{{ $t('monitor.temperature') }}
<el-tooltip placement="top" :content="$t('ai_tools.gpu.temperatureHelper')">
<el-icon class="icon-item"><InfoFilled /></el-icon>
</el-tooltip>
</div>
</template>
{{ item.stats.temperature }}
</el-descriptions-item>
<el-descriptions-item :label="$t('monitor.powerUsage')">
{{ item.stats.power }}
</el-descriptions-item>
<el-descriptions-item :label="$t('monitor.memoryUsage')">
{{ item.stats.memoryUsed }} / {{ item.basic.memory }}
</el-descriptions-item>
<el-descriptions-item :label="$t('ai_tools.gpu.busID')">
{{ item.basic.pciBdfAddress }}
</el-descriptions-item>
</el-descriptions>
<div class="mt-5">
<span class="title-class">{{ $t('ai_tools.gpu.process') }}</span>
</div>
<el-table :data="item.processes" v-if="item.processes?.length !== 0">
<el-table-column label="PID" prop="pid" />
<el-table-column :label="$t('ai_tools.gpu.processName')" prop="command" />
<el-table-column :label="$t('ai_tools.gpu.shr')" prop="shr" />
<el-table-column :label="$t('ai_tools.gpu.processMemoryUsage')" prop="memory" />
</el-table>
</el-collapse-item>
</el-collapse>
</template>
</LayoutContent>
</div>
<LayoutContent
:title="$t('ai_tools.gpu.gpu')"
:divider="true"
v-if="gpuInfo.driverVersion.length === 0 && xpuInfo.driverVersion.length == 0 && !loading"
>
<template #main>
<div class="app-warn">
<div class="flx-center">
<span>{{ $t('ai_tools.gpu.gpuHelper') }}</span>
</div>
<div>
<img src="@/assets/images/no_app.svg" />
</div>
</div>
</template>
</LayoutContent>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { loadGPUInfo } from '@/api/modules/ai-tool';
import { AITool } from '@/api/interface/ai-tool';
import i18n from '@/lang';
const loading = ref();
const activeNames = ref(0);
const gpuInfo = ref<AITool.Info>({
cudaVersion: '',
driverVersion: '',
type: 'nvidia',
gpu: [],
});
const xpuInfo = ref<AITool.XpuInfo>({
driverVersion: '',
type: 'xpu',
xpu: [],
});
const gpuType = ref('nvidia');
const search = async () => {
loading.value = true;
await loadGPUInfo()
.then((res) => {
loading.value = false;
gpuType.value = res.data.type;
if (res.data.type == 'nvidia') {
gpuInfo.value = res.data;
} else {
xpuInfo.value = res.data;
}
})
.catch(() => {
loading.value = false;
});
};
const refresh = async () => {
const res = await loadGPUInfo();
gpuInfo.value = res.data;
};
const lowerCase = (val: string) => {
return val.toLowerCase();
};
const loadComputeMode = (val: string) => {
switch (val) {
case 'Default':
return i18n.global.t('ai_tools.gpu.default');
case 'Exclusive Process':
return i18n.global.t('ai_tools.gpu.exclusiveProcess');
case 'Exclusive Thread':
return i18n.global.t('ai_tools.gpu.exclusiveThread');
case 'Prohibited':
return i18n.global.t('ai_tools.gpu.prohibited');
}
};
const loadEcc = (val: string) => {
if (val === 'N/A') {
return i18n.global.t('ai_tools.gpu.migModeNA');
}
if (val === 'Disabled') {
return i18n.global.t('ai_tools.gpu.disabled');
}
if (val === 'Enabled') {
return i18n.global.t('ai_tools.gpu.enabled');
}
return val;
};
const loadProcessType = (val: string) => {
if (val === 'C' || val === 'G') {
return i18n.global.t('ai_tools.gpu.type' + val);
}
if (val === 'C+G') {
return i18n.global.t('ai_tools.gpu.typeCG');
}
return val;
};
onMounted(() => {
search();
});
</script>
<style lang="scss" scoped>
.name-class {
font-size: 18px;
font-weight: 500;
}
.title-class {
font-size: 14px;
font-weight: 500;
}
.cell-item {
display: flex;
align-items: center;
.icon-item {
margin-left: 4px;
margin-top: -1px;
}
}
</style>

View file

@ -0,0 +1,94 @@
<template>
<el-drawer
v-model="drawerVisible"
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
size="30%"
>
<template #header>
<DrawerHeader :header="$t('ai_tools.model.create')" :back="handleClose" />
</template>
<el-row type="flex" justify="center">
<el-col :span="22">
<el-alert type="info" :closable="false">
<template #title>
<span class="flx-align-center">
{{ $t('ai_tools.model.create_helper') }}
<el-link class="ml-5" icon="Position" @click="goSearch()" type="primary">
{{ $t('firewall.quickJump') }}
</el-link>
</span>
</template>
</el-alert>
<el-form ref="formRef" label-position="top" :model="form">
<el-form-item :label="$t('commons.table.name')" :rules="Rules.requiredInput" prop="name">
<el-input v-model.trim="form.name" />
<span class="input-help" v-if="form.name">
ollama pull {{ form.name.replaceAll('ollama run ', '').replaceAll('ollama pull ', '') }}
</span>
</el-form-item>
</el-form>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.add') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { createOllamaModel } from '@/api/modules/ai-tool';
const drawerVisible = ref(false);
const form = reactive({
name: '',
});
const acceptParams = async (): Promise<void> => {
form.name = '';
drawerVisible.value = true;
};
const emit = defineEmits(['search', 'log']);
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
let itemName = form.name.replaceAll('ollama run ', '').replaceAll('ollama pull ', '');
await createOllamaModel(itemName);
drawerVisible.value = false;
emit('search');
emit('log', itemName);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
});
};
const goSearch = () => {
window.open('https://ollama.com/search', '_blank', 'noopener,noreferrer');
};
const handleClose = () => {
drawerVisible.value = false;
};
defineExpose({
acceptParams,
});
</script>

View file

@ -0,0 +1,264 @@
<template>
<div v-loading="loading">
<RouterButton
:buttons="[
{
label: i18n.global.t('ai_tools.model.model'),
path: '/ai-tools/model',
},
]"
/>
<LayoutContent title="Ollama">
<template #app>
<AppStatus
app-key="ollama"
v-model:loading="loading"
:hide-setting="true"
v-model:mask-show="maskShow"
@is-exist="checkExist"
ref="appStatusRef"
></AppStatus>
</template>
<template #toolbar>
<div class="flex justify-between gap-2 flex-wrap sm:flex-row">
<div class="flex flex-wrap gap-3">
<el-button v-if="modelInfo.status === 'Running'" type="primary" @click="onCreate()">
{{ $t('ai_tools.model.create') }}
</el-button>
<!-- <el-button @click="onLoadConn" type="primary" plain>
{{ $t('database.databaseConnInfo') }}
</el-button> -->
<el-button icon="Position" @click="goDashboard()" type="primary" plain>OpenWebUI</el-button>
</div>
<div>
<TableSearch @search="search()" v-model:searchName="searchName" />
</div>
</div>
</template>
<template #main>
<ComplexTable
:pagination-config="paginationConfig"
:class="{ mask: maskShow }"
@sort-change="search"
@search="search"
:data="data"
>
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="90" />
<el-table-column :label="$t('file.size')" prop="size" />
<el-table-column :label="$t('commons.button.log')">
<template #default="{ row }">
<el-button @click="onLoadLog(row.name)" link type="primary">
{{ $t('website.check') }}
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.createdAt')" prop="modified" />
<fu-table-operations
:ellipsis="mobile ? 0 : 10"
:min-width="mobile ? 'auto' : 400"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<el-card v-if="modelInfo.status != 'Running' && !loading && maskShow" class="mask-prompt">
<span>
{{ $t('commons.service.serviceNotStarted', ['Ollama']) }}
</span>
</el-card>
<LayoutContent v-if="!modelInfo.isExist && !loading" title="Ollama" :divider="true">
<template #main>
<div class="app-warn">
<div class="flex flex-col gap-2 items-center justify-center w-full sm:flex-row">
<span>{{ $t('app.checkInstalledWarn', [$t('database.noMysql')]) }}</span>
<span @click="goInstall('ollama')" class="flex items-center justify-center gap-0.5">
<el-icon><Position /></el-icon>
{{ $t('database.goInstall') }}
</span>
</div>
<div>
<img src="@/assets/images/no_app.svg" />
</div>
</div>
</template>
</LayoutContent>
<el-dialog
v-model="dashboardVisible"
:title="$t('app.checkTitle')"
width="30%"
:close-on-click-modal="false"
:destroy-on-close="true"
>
<div class="flex justify-center items-center gap-2 flex-wrap">
{{ $t('app.checkInstalledWarn', ['OpenWebUI']) }}
<el-link icon="Position" @click="goInstall('ollama-webui')" type="primary">
{{ $t('database.goInstall') }}
</el-link>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dashboardVisible = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</el-dialog>
<AddDialog ref="addRef" @search="search" @log="onLoadLog" />
<Log ref="logRef" @close="search" />
<PortJumpDialog ref="dialogPortJumpRef" />
</div>
</template>
<script lang="ts" setup>
import AppStatus from '@/components/app-status/index.vue';
import AddDialog from '@/views/ai-tools/model/add/index.vue';
import Log from '@/components/log-dialog/index.vue';
import PortJumpDialog from '@/components/port-jump/index.vue';
import { computed, onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { App } from '@/api/interface/app';
import { GlobalStore } from '@/store';
import { deleteOllamaModel, searchOllamaModel } from '@/api/modules/ai-tool';
import { AITool } from '@/api/interface/ai-tool';
import { GetAppPort } from '@/api/modules/app';
import router from '@/routers';
import { MsgSuccess } from '@/utils/message';
const globalStore = GlobalStore();
const loading = ref(false);
const maskShow = ref(true);
const addRef = ref();
const logRef = ref();
const openWebUIPort = ref();
const dashboardVisible = ref(false);
const dialogPortJumpRef = ref();
const appStatusRef = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'model-page-size',
currentPage: 1,
pageSize: Number(localStorage.getItem('page-size')) || 10,
total: 0,
});
const searchName = ref();
const modelInfo = reactive({
status: '',
container: '',
isExist: null,
version: '',
});
const mobile = computed(() => {
return globalStore.isMobile();
});
const search = async () => {
let params = {
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
info: searchName.value,
};
loading.value = true;
await searchOllamaModel(params)
.then((res) => {
loading.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
};
const onCreate = async () => {
addRef.value.acceptParams();
};
const goDashboard = async () => {
if (openWebUIPort.value === 0) {
dashboardVisible.value = true;
return;
}
dialogPortJumpRef.value.acceptParams({ port: openWebUIPort.value });
};
const goInstall = (name: string) => {
router.push({ name: 'AppAll', query: { install: name } });
};
const loadWebUIPort = async () => {
const res = await GetAppPort('ollama-webui', '');
openWebUIPort.value = res.data;
};
const checkExist = (data: App.CheckInstalled) => {
modelInfo.isExist = data.isExist;
modelInfo.status = data.status;
modelInfo.version = data.version;
modelInfo.container = data.containerName;
};
const onDelete = async (row: AITool.OllamaModelInfo) => {
ElMessageBox.confirm(i18n.global.t('commons.msg.delete'), i18n.global.t('commons.button.delete'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
await deleteOllamaModel(row.name)
.then(() => {
loading.value = false;
search();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
};
const onLoadLog = (name: string) => {
logRef.value.acceptParams({ id: 0, type: 'ollama-model', name: name, tail: true });
};
const buttons = [
{
label: i18n.global.t('commons.button.delete'),
click: (row: AITool.OllamaModelInfo) => {
onDelete(row);
},
},
];
onMounted(() => {
search();
loadWebUIPort();
});
</script>
<style lang="scss" scoped>
.iconInTable {
margin-left: 5px;
margin-top: 3px;
}
.jumpAdd {
margin-top: 10px;
margin-left: 15px;
margin-bottom: 5px;
font-size: 12px;
}
.tagClass {
float: right;
font-size: 12px;
margin-top: 5px;
}
</style>