feat(ai): add unified GPU configuration and collapsible deployment cards (#14253)

This commit is contained in:
ssongliu
2026-10-30 17:03:21 +08:00
committed by GitHub
parent 2d473042fc
commit 2f7bab67b3
38 changed files with 1868 additions and 398 deletions
+5
View File
@@ -3,11 +3,16 @@ package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) LoadModelGPUDevices(c *gin.Context) {
helper.SuccessWithData(c, service.LoadModelGPUDevices())
}
// @Tags AI
// @Summary Load gpu / xpu info
// @Accept json
+22
View File
@@ -2,6 +2,28 @@ package dto
import "time"
type ModelGPUDevice struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
UUID string `json:"uuid,omitempty"`
Available bool `json:"available"`
}
type ModelGPUDevices struct {
Devices []ModelGPUDevice `json:"devices"`
Warnings []string `json:"warnings"`
}
type ModelGPUConfig struct {
Mode string `json:"mode"`
Count int `json:"count,omitempty"`
Error string `json:"error,omitempty"`
Enabled bool `json:"enabled"`
DeviceIDs []string `json:"deviceIDs"`
Capabilities []string `json:"capabilities"`
}
type OllamaModelInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
+4
View File
@@ -22,6 +22,9 @@ type AppInstallCreate struct {
Name string `json:"name" validate:"required"`
Services map[string]string `json:"services"`
TaskID string `json:"taskID"`
GPU *dto.ModelGPUConfig `json:"gpu"`
GPUImageType string `json:"-"`
SyncModelAccount *bool `json:"syncModelAccount"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
@@ -128,6 +131,7 @@ type AppInstallDelete struct {
}
type AppInstalledUpdate struct {
GPU *dto.ModelGPUConfig `json:"gpu"`
SyncModelAccount *bool `json:"syncModelAccount"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
+9 -8
View File
@@ -8,14 +8,15 @@ type TensorRTLLMSearch struct {
}
type TensorRTLLMCreate struct {
Name string `json:"name" validate:"required"`
ContainerName string `json:"containerName" validate:"required"`
Version string `json:"version" validate:"required"`
ModelDir string `json:"modelDir" validate:"required"`
Image string `json:"image" validate:"required"`
Command string `json:"command" validate:"required"`
ModelType string `json:"modelType"`
ModelSpeedup bool `json:"modelSpeedup"`
Name string `json:"name" validate:"required"`
ContainerName string `json:"containerName" validate:"required"`
Version string `json:"version" validate:"required"`
ModelDir string `json:"modelDir" validate:"required"`
Image string `json:"image" validate:"required"`
Command string `json:"command" validate:"required"`
ModelType string `json:"modelType"`
ModelSpeedup bool `json:"modelSpeedup"`
GPU *dto.ModelGPUConfig `json:"gpu"`
DockerConfig
}
+9 -8
View File
@@ -186,13 +186,14 @@ type AppParam struct {
}
type AppConfig struct {
AppKey string `json:"appKey"`
Name string `json:"name"`
HttpPort int `json:"httpPort"`
SyncModelAccount bool `json:"syncModelAccount"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
Params []AppParam `json:"params"`
RawCompose string `json:"rawCompose"`
GPU *dto.ModelGPUConfig `json:"gpu,omitempty"`
AppKey string `json:"appKey"`
Name string `json:"name"`
HttpPort int `json:"httpPort"`
SyncModelAccount bool `json:"syncModelAccount"`
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
Params []AppParam `json:"params"`
RawCompose string `json:"rawCompose"`
request.AppContainerConfig
}
+8 -6
View File
@@ -1,6 +1,7 @@
package response
import (
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
@@ -12,12 +13,13 @@ type TensorRTLLMsRes struct {
type TensorRTLLMDTO struct {
model.TensorRTLLM
Version string `json:"version"`
Model string `json:"model"`
Dir string `json:"dir"`
ModelDir string `json:"modelDir"`
Image string `json:"image"`
Command string `json:"command"`
Version string `json:"version"`
Model string `json:"model"`
Dir string `json:"dir"`
ModelDir string `json:"modelDir"`
Image string `json:"image"`
Command string `json:"command"`
GPU *dto.ModelGPUConfig `json:"gpu"`
ExposedPorts []request.ExposedPort `json:"exposedPorts"`
Environments []request.Environment `json:"environments"`
+20
View File
@@ -542,6 +542,26 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
composeByte []byte
paramByte []byte
)
if (app.Key == "ollama" || app.Key == "vllm") && req.GPU != nil {
gpuServiceName := appInstall.ServiceName
imageType := "nvidia"
if app.Key == "vllm" {
imageType = req.GPUImageType
if len(servicesMap) > 1 {
gpuServiceName = "vllm"
if _, ok := servicesMap[gpuServiceName]; !ok {
gpuServiceName = "vllm-dspark"
}
}
}
service, ok := servicesMap[gpuServiceName].(map[string]interface{})
if !ok {
return nil, buserr.New("ErrFileParse")
}
if err = ApplyModelGPU(service, req.GPU, req.Params, imageType); err != nil {
return
}
}
composeByte, err = yaml.Marshal(composeMap)
if err != nil {
+33 -3
View File
@@ -493,9 +493,10 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
}
backupDockerCompose := installed.DockerCompose
if req.Advanced {
updateGPU := installed.App.Key == "ollama" && req.GPU != nil
if req.Advanced || updateGPU {
composeMap := make(map[string]interface{})
if req.EditCompose {
if req.Advanced && req.EditCompose {
if err = yaml.Unmarshal([]byte(req.DockerCompose), &composeMap); err != nil {
return err
}
@@ -504,16 +505,35 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
return err
}
}
if !req.SkipComposeCommonConfig {
if req.Advanced && !req.SkipComposeCommonConfig {
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
return err
}
}
if updateGPU {
services, _ := composeMap["services"].(map[string]interface{})
service, ok := services[installed.ServiceName].(map[string]interface{})
if !ok {
return buserr.New("ErrFileParse")
}
gpuParams := map[string]interface{}{}
if err := json.Unmarshal([]byte(installed.Env), &gpuParams); err != nil {
return err
}
for key, value := range req.Params {
gpuParams[key] = value
}
if err := ApplyModelGPU(service, req.GPU, gpuParams, "nvidia"); err != nil {
return err
}
}
composeByte, err := yaml.Marshal(composeMap)
if err != nil {
return err
}
installed.DockerCompose = string(composeByte)
}
if req.Advanced {
if req.ContainerName == "" {
req.Params[constant.ContainerName] = installed.ContainerName
} else {
@@ -1013,6 +1033,16 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
res.ModelAccountBaseURLType = install.ModelAccountBaseURLType
res.ModelAccountBaseURL = install.ModelAccountBaseURL
if install.App.Key == "ollama" {
var project map[string]interface{}
if err := yaml.Unmarshal([]byte(install.DockerCompose), &project); err != nil {
return nil, err
}
services, _ := project["services"].(map[string]interface{})
service, ok := services[install.ServiceName].(map[string]interface{})
if !ok {
return nil, buserr.New("ErrFileParse")
}
res.GPU = ReadModelGPU(service, envs, "nvidia")
res.ModelAccountBaseURLType, res.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(install)
if err != nil {
return nil, err
+663
View File
@@ -0,0 +1,663 @@
package service
import (
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/compose-spec/compose-go/v2/interpolation"
"github.com/compose-spec/compose-go/v2/template"
)
func LoadModelGPUDevices() dto.ModelGPUDevices {
result := dto.ModelGPUDevices{Devices: []dto.ModelGPUDevice{}, Warnings: []string{}}
uuidPattern := re.GetRegex(re.NvidiaGPUUUIDPattern)
renderPattern := re.GetRegex(re.IntelGPURenderNamePattern)
ascendPattern := re.GetRegex(re.AscendGPUDeviceNamePattern)
for _, directory := range []string{"/proc/driver/nvidia/gpus", "/sys/class/drm", "/dev"} {
entries, err := os.ReadDir(directory)
if err != nil {
if !os.IsNotExist(err) {
result.Warnings = append(result.Warnings, err.Error())
}
continue
}
for _, entry := range entries {
var device dto.ModelGPUDevice
var devicePath string
switch directory {
case "/proc/driver/nvidia/gpus":
informationPath := filepath.Join(directory, entry.Name(), "information")
content, err := os.ReadFile(informationPath)
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
continue
}
fields := map[string]string{}
for _, line := range strings.Split(string(content), "\n") {
key, value, found := strings.Cut(line, ":")
if found {
fields[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
}
uuid := fields["GPU UUID"]
minor, err := strconv.Atoi(fields["Device Minor"])
if !uuidPattern.MatchString(uuid) || err != nil || minor < 0 {
result.Warnings = append(result.Warnings, fmt.Sprintf("invalid NVIDIA GPU UUID or device minor in %s", informationPath))
continue
}
name := fields["Model"]
if name == "" {
name = "NVIDIA"
}
device = dto.ModelGPUDevice{ID: "nvidia:" + uuid, Name: name + " · " + entry.Name(), Type: "nvidia", UUID: uuid}
devicePath = filepath.Join("/dev", "nvidia"+strconv.Itoa(minor))
case "/sys/class/drm":
if !renderPattern.MatchString(entry.Name()) {
continue
}
vendor, err := os.ReadFile(filepath.Join(directory, entry.Name(), "device/vendor"))
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
continue
}
if strings.TrimSpace(string(vendor)) != "0x8086" {
continue
}
devicePath = filepath.Join("/dev/dri", entry.Name())
device = dto.ModelGPUDevice{ID: "intel:" + devicePath, Name: "Intel · " + entry.Name(), Type: "intel"}
case "/dev":
if !ascendPattern.MatchString(entry.Name()) {
continue
}
devicePath = filepath.Join(directory, entry.Name())
device = dto.ModelGPUDevice{ID: "ascend:" + strings.TrimPrefix(entry.Name(), "davinci"), Name: "Ascend · " + entry.Name(), Type: "ascend"}
}
info, err := os.Stat(devicePath)
if err != nil {
result.Warnings = append(result.Warnings, err.Error())
} else {
device.Available = info.Mode()&os.ModeCharDevice != 0
}
result.Devices = append(result.Devices, device)
}
}
return result
}
func ApplyModelGPU(service map[string]interface{}, config *dto.ModelGPUConfig, params map[string]interface{}, imageType string) error {
if config == nil {
return nil
}
if err := validateModelGPU(config, imageType); err != nil {
return err
}
resolved, err := resolveModelGPUService(service, params, false)
if err != nil {
return err
}
devicePaths := []string{}
ids := []string{}
logicalIDs := []string{}
if config.Enabled && imageType != "nvidia" {
for _, deviceID := range config.DeviceIDs {
id := strings.TrimPrefix(deviceID, imageType+":")
if !slices.Contains(ids, id) {
ids = append(ids, id)
}
}
if imageType == "ascend" {
slices.SortFunc(ids, func(a, b string) int {
left, _ := strconv.Atoi(a)
right, _ := strconv.Atoi(b)
return left - right
})
visibleIDs := ids
if config.Mode == "all" || resolved["privileged"] == true || resolved["privileged"] == "true" {
visibleIDs = loadModelAscendDeviceIDs()
if len(visibleIDs) == 0 {
return fmt.Errorf("no available Ascend devices")
}
}
if config.Mode == "all" {
ids = visibleIDs
} else {
for _, id := range ids {
index := slices.Index(visibleIDs, id)
if index < 0 {
return fmt.Errorf("Ascend GPU is unavailable: %s", id)
}
logicalIDs = append(logicalIDs, strconv.Itoa(index))
}
}
for _, id := range ids {
devicePaths = append(devicePaths, "/dev/davinci"+id)
}
devicePaths = append(devicePaths, "/dev/davinci_manager", "/dev/devmm_svm", "/dev/hisi_hdc")
} else if config.Mode == "all" {
devicePaths = append(devicePaths, "/dev/dri")
} else {
devicePaths = append(devicePaths, ids...)
}
}
if imageType == "nvidia" {
delete(service, "gpus")
}
if imageType == "nvidia" && resolved["runtime"] == "nvidia" && !config.Enabled {
delete(service, "runtime")
}
deploy, _ := service["deploy"].(map[string]interface{})
resources, _ := deploy["resources"].(map[string]interface{})
reservations, _ := resources["reservations"].(map[string]interface{})
devices, _ := reservations["devices"].([]interface{})
resolvedDevices, _ := resolved["reservations"].([]interface{})
kept := []interface{}{}
var gpuDevice map[string]interface{}
for index, item := range devices {
device, _ := resolvedDevices[index].(map[string]interface{})
capabilities, _ := device["capabilities"].([]interface{})
driver, _ := device["driver"].(string)
isNvidiaGPU := driver == "nvidia" || (driver == "" && slices.Contains(capabilities, interface{}("gpu")))
if imageType != "nvidia" || !isNvidiaGPU {
kept = append(kept, item)
} else if gpuDevice == nil {
gpuDevice, _ = item.(map[string]interface{})
}
}
if config.Enabled && imageType == "nvidia" {
device := gpuDevice
if device == nil {
device = map[string]interface{}{}
}
device["driver"] = "nvidia"
device["capabilities"] = config.Capabilities
delete(device, "count")
delete(device, "device_ids")
switch config.Mode {
case "all":
device["count"] = "all"
case "count":
device["count"] = config.Count
default:
ids := []string{}
for _, id := range config.DeviceIDs {
ids = append(ids, strings.TrimPrefix(id, "nvidia:"))
}
device["device_ids"] = ids
}
kept = append(kept, device)
}
if len(kept) > 0 {
if deploy == nil {
deploy = map[string]interface{}{}
service["deploy"] = deploy
}
if resources == nil {
resources = map[string]interface{}{}
deploy["resources"] = resources
}
if reservations == nil {
reservations = map[string]interface{}{}
resources["reservations"] = reservations
}
reservations["devices"] = kept
} else {
delete(reservations, "devices")
if len(reservations) == 0 {
delete(resources, "reservations")
}
if len(resources) == 0 {
delete(deploy, "resources")
}
if len(deploy) == 0 {
delete(service, "deploy")
}
}
mappedPaths := []string{}
for _, key := range []string{"devices", "volumes"} {
mappings, _ := service[key].([]interface{})
resolvedMappings, _ := resolved[key].([]interface{})
kept := []interface{}{}
for index, item := range mappings {
source := modelGPUDeviceSource(resolvedMappings[index])
managed := false
switch imageType {
case "nvidia":
managed = strings.HasPrefix(source, "/dev/nvidia") || strings.HasPrefix(source, "nvidia.com/gpu=")
case "intel":
managed = source == "/dev/dri" || strings.HasPrefix(source, "/dev/dri/")
case "ascend":
managed = strings.HasPrefix(source, "/dev/davinci") || source == "/dev/devmm_svm" || source == "/dev/hisi_hdc"
}
if managed {
selected := slices.Contains(devicePaths, source)
if config.Enabled && config.Mode == "all" && imageType == "intel" {
selected = true
}
if selected {
kept = append(kept, item)
mappedPaths = append(mappedPaths, source)
}
continue
}
kept = append(kept, item)
}
if len(kept) == 0 {
delete(service, key)
} else {
service[key] = kept
}
}
if config.Enabled && imageType != "nvidia" {
devices, _ := service["devices"].([]interface{})
for _, path := range devicePaths {
if !slices.Contains(mappedPaths, path) {
devices = append(devices, path+":"+path)
}
}
if len(devices) > 0 {
service["devices"] = devices
}
}
gpuEnvironment := map[string]string{}
if config.Enabled && imageType == "ascend" && config.Mode != "all" {
gpuEnvironment["ASCEND_RT_VISIBLE_DEVICES"] = strings.Join(logicalIDs, ",")
}
var environmentKeys []string
switch imageType {
case "nvidia":
environmentKeys = []string{"NVIDIA_VISIBLE_DEVICES", "NVIDIA_DRIVER_CAPABILITIES"}
case "ascend":
environmentKeys = []string{"ASCEND_RT_VISIBLE_DEVICES"}
if params != nil {
params["ASCEND_RT_VISIBLE_DEVICES"] = strings.Join(logicalIDs, ",")
}
default:
return nil
}
switch environment := service["environment"].(type) {
case map[string]interface{}:
for _, key := range environmentKeys {
delete(environment, key)
}
for key, value := range gpuEnvironment {
environment[key] = value
}
case []interface{}:
kept := []interface{}{}
for _, item := range environment {
value, ok := item.(string)
if !ok {
return fmt.Errorf("invalid Compose environment")
}
key, _, _ := strings.Cut(value, "=")
if !slices.Contains(environmentKeys, key) {
kept = append(kept, item)
}
}
for key, value := range gpuEnvironment {
kept = append(kept, key+"="+value)
}
service["environment"] = kept
case nil:
if len(gpuEnvironment) > 0 {
environment := map[string]interface{}{}
for key, value := range gpuEnvironment {
environment[key] = value
}
service["environment"] = environment
}
default:
return fmt.Errorf("invalid Compose environment")
}
return nil
}
func ReadModelGPU(service map[string]interface{}, params map[string]interface{}, imageType string) *dto.ModelGPUConfig {
resolved, err := resolveModelGPUService(service, params, true)
if err != nil {
return &dto.ModelGPUConfig{Mode: "devices", DeviceIDs: []string{}, Capabilities: []string{"gpu"}, Error: err.Error()}
}
if imageType == "nvidia" {
config, err := readModelGPU(service, resolved)
if err != nil {
return &dto.ModelGPUConfig{Mode: "devices", DeviceIDs: []string{}, Capabilities: []string{"gpu"}, Error: err.Error()}
}
return config
}
config := &dto.ModelGPUConfig{Mode: "devices", DeviceIDs: []string{}, Capabilities: []string{"gpu"}}
privileged := resolved["privileged"] == true || resolved["privileged"] == "true"
all := false
for _, key := range []string{"devices", "volumes"} {
mappings, _ := resolved[key].([]interface{})
for _, item := range mappings {
source := modelGPUDeviceSource(item)
id := ""
if imageType == "intel" {
switch {
case source == "/dev/dri":
all = true
case re.GetRegex(re.IntelGPUDevicePathPattern).MatchString(source):
id = imageType + ":" + source
case strings.HasPrefix(source, "/dev/dri/"):
config.Error = "unsupported Intel GPU mapping requires an explicit selection: " + source
return config
}
}
if imageType == "ascend" && re.GetRegex(re.AscendGPUDevicePathPattern).MatchString(source) {
id = imageType + ":" + strings.TrimPrefix(source, "/dev/davinci")
}
if id != "" && !slices.Contains(config.DeviceIDs, id) {
config.DeviceIDs = append(config.DeviceIDs, id)
}
}
}
config.Enabled = all || len(config.DeviceIDs) > 0
config.Mode = "devices"
if all {
config.Mode = "all"
config.DeviceIDs = []string{}
}
if imageType == "ascend" {
const key = "ASCEND_RT_VISIBLE_DEVICES"
visible := ""
switch environment := service["environment"].(type) {
case map[string]interface{}:
if value, exists := environment[key]; exists {
if value == nil {
visible = "${" + key + "}"
} else {
visible = fmt.Sprint(value)
}
}
case []interface{}:
for _, item := range environment {
value, _ := item.(string)
if value == key {
visible = "${" + key + "}"
} else if strings.HasPrefix(value, key+"=") {
visible = strings.TrimPrefix(value, key+"=")
}
}
}
visible, err := resolveModelGPUValue(visible, params)
if err != nil {
config.Error = err.Error()
return config
}
visible = strings.TrimSpace(visible)
if visible == "all" || (visible == "" && config.Enabled) {
config.Enabled = true
config.Mode = "all"
config.DeviceIDs = []string{}
} else if visible != "" {
physicalIDs := []string{}
for _, id := range config.DeviceIDs {
physicalIDs = append(physicalIDs, strings.TrimPrefix(id, "ascend:"))
}
slices.SortFunc(physicalIDs, func(a, b string) int {
left, _ := strconv.Atoi(a)
right, _ := strconv.Atoi(b)
return left - right
})
if privileged {
physicalIDs = loadModelAscendDeviceIDs()
}
selected := []string{}
for _, id := range strings.Split(visible, ",") {
id = strings.TrimSpace(id)
if !re.GetRegex(re.AscendGPUDeviceIDPattern).MatchString(id) {
config.Error = "invalid Ascend GPU device: " + id
return config
}
index, err := strconv.Atoi(id)
if err != nil || index < 0 || index >= len(physicalIDs) {
config.Error = "Ascend logical GPU is not available in the container: " + id
return config
}
selected = append(selected, "ascend:"+physicalIDs[index])
}
config.Enabled = true
config.Mode = "devices"
config.DeviceIDs = selected
}
}
return config
}
func readModelGPU(service, resolved map[string]interface{}) (*dto.ModelGPUConfig, error) {
config := &dto.ModelGPUConfig{Mode: "all", DeviceIDs: []string{}, Capabilities: []string{"gpu"}}
devices, _ := resolved["reservations"].([]interface{})
if service["gpus"] != nil {
return nil, fmt.Errorf("custom GPU declarations require an explicit selection")
}
for _, item := range devices {
device, ok := item.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid GPU reservation")
}
driver, _ := device["driver"].(string)
capabilities, _ := device["capabilities"].([]interface{})
isGPU := driver == "nvidia" || (driver == "" && slices.Contains(capabilities, interface{}("gpu")))
if !isGPU {
continue
}
if config.Enabled {
return nil, fmt.Errorf("multiple GPU declarations require an explicit selection")
}
config.Enabled = true
ids, _ := device["device_ids"].([]interface{})
if len(ids) > 0 {
if device["count"] != nil {
return nil, fmt.Errorf("GPU count and device_ids cannot be combined")
}
config.Mode = "devices"
for _, id := range ids {
config.DeviceIDs = append(config.DeviceIDs, "nvidia:"+fmt.Sprint(id))
}
} else if count := device["count"]; count != nil && fmt.Sprint(count) != "all" && fmt.Sprint(count) != "-1" {
config.Mode = "count"
countValue, err := strconv.Atoi(fmt.Sprint(count))
config.Count = countValue
if err != nil {
return nil, fmt.Errorf("invalid GPU count: %v", count)
}
}
for _, capability := range capabilities {
value, ok := capability.(string)
if !ok {
return nil, fmt.Errorf("unsupported GPU capabilities: %v", capabilities)
}
if !slices.Contains(config.Capabilities, value) {
config.Capabilities = append(config.Capabilities, value)
}
}
}
for _, key := range []string{"devices", "volumes"} {
mappings, _ := resolved[key].([]interface{})
for _, item := range mappings {
source := modelGPUDeviceSource(item)
if strings.HasPrefix(source, "/dev/nvidia") || strings.HasPrefix(source, "nvidia.com/gpu=") {
return nil, fmt.Errorf("direct NVIDIA device mappings require an explicit selection")
}
}
}
if !config.Enabled && resolved["runtime"] == "nvidia" {
return nil, fmt.Errorf("custom NVIDIA runtime configuration requires an explicit selection")
}
if err := validateModelGPU(config, "nvidia"); err != nil {
return nil, err
}
return config, nil
}
func validateModelGPU(config *dto.ModelGPUConfig, imageType string) error {
if config.Error != "" {
return fmt.Errorf("GPU configuration must be selected explicitly: %s", config.Error)
}
if !config.Enabled {
return nil
}
patterns := map[string]string{
"nvidia": re.NvidiaGPUDeviceIDPattern,
"intel": re.IntelGPUDevicePathPattern,
"ascend": re.AscendGPUDeviceIDPattern,
}
pattern, supported := patterns[imageType]
if !supported {
return fmt.Errorf("unsupported GPU type: %s", imageType)
}
switch config.Mode {
case "all":
if config.Count != 0 || len(config.DeviceIDs) != 0 {
return fmt.Errorf("all GPUs cannot be combined with count or device IDs")
}
case "count":
if imageType != "nvidia" || config.Count < 1 || len(config.DeviceIDs) != 0 {
return fmt.Errorf("GPU count requires a positive number and runtime device allocation")
}
case "devices", "":
if config.Count != 0 || len(config.DeviceIDs) == 0 {
return fmt.Errorf("select at least one GPU device")
}
for _, deviceID := range config.DeviceIDs {
id := strings.TrimPrefix(deviceID, imageType+":")
if !re.GetRegex(pattern).MatchString(id) {
return fmt.Errorf("invalid %s GPU device: %s", imageType, deviceID)
}
}
default:
return fmt.Errorf("unsupported GPU selection mode: %s", config.Mode)
}
if imageType != "nvidia" {
return nil
}
if !slices.Contains(config.Capabilities, "gpu") {
return fmt.Errorf("NVIDIA capabilities must include gpu")
}
for _, capability := range config.Capabilities {
if capability != "gpu" && capability != "utility" && capability != "compute" {
return fmt.Errorf("unsupported NVIDIA capability: %s", capability)
}
}
return nil
}
func resolveModelGPUService(service map[string]interface{}, params map[string]interface{}, resolveSelection bool) (map[string]interface{}, error) {
fields := map[string]interface{}{}
fields["runtime"] = service["runtime"]
deploy, _ := service["deploy"].(map[string]interface{})
resources, _ := deploy["resources"].(map[string]interface{})
reservations, _ := resources["reservations"].(map[string]interface{})
fields["reservations"] = reservations["devices"]
if devices, ok := reservations["devices"].([]interface{}); ok && !resolveSelection {
declarations := make([]interface{}, 0, len(devices))
for _, item := range devices {
device, _ := item.(map[string]interface{})
declarations = append(declarations, map[string]interface{}{"driver": device["driver"], "capabilities": device["capabilities"]})
}
fields["reservations"] = declarations
}
resolved, err := interpolation.Interpolate(fields, interpolation.Options{
Substitute: func(value string, _ template.Mapping) (string, error) { return resolveModelGPUValue(value, params) },
})
if err != nil {
return nil, err
}
for _, key := range []string{"devices", "volumes"} {
mappings, _ := service[key].([]interface{})
sources := make([]interface{}, 0, len(mappings))
for _, item := range mappings {
source, err := template.SubstituteWithOptions(modelGPUDeviceSource(item), func(name string) (string, bool) {
value, ok := params[name]
if !ok || value == nil {
return "", false
}
return fmt.Sprint(value), true
}, template.WithoutLogging)
if err != nil {
return nil, err
}
if _, short := item.(string); short {
source = modelGPUDeviceSource(source)
}
sources = append(sources, map[string]interface{}{"source": source})
}
resolved[key] = sources
}
resolved["privileged"] = service["privileged"]
if value, ok := service["privileged"].(string); ok {
if value, err := resolveModelGPUValue(value, params); err == nil {
resolved["privileged"] = value
}
}
return resolved, nil
}
func modelGPUDeviceSource(item interface{}) string {
switch mapping := item.(type) {
case string:
if strings.HasPrefix(mapping, "nvidia.com/gpu=") {
return mapping
}
depth := 0
for index, ch := range mapping {
switch ch {
case '{':
depth++
case '}':
depth--
case ':':
if depth == 0 {
return mapping[:index]
}
}
}
return mapping
case map[string]interface{}:
source, _ := mapping["source"].(string)
return source
}
return ""
}
func resolveModelGPUValue(value string, params map[string]interface{}) (string, error) {
return template.SubstituteWithOptions(value, func(name string) (string, bool) {
value, ok := params[name]
if !ok || value == nil {
return "", false
}
return fmt.Sprint(value), true
}, template.WithoutLogging, template.WithReplacementFunction(func(value string, mapping template.Mapping, config *template.Config) (string, error) {
resolved, applied, err := template.DefaultReplacementAppliedFunc(value, mapping, config)
if err != nil {
return "", err
}
if !applied {
return "", fmt.Errorf("unresolved GPU variable: %s", value)
}
return resolved, nil
}))
}
func loadModelAscendDeviceIDs() []string {
ids := []string{}
for _, device := range LoadModelGPUDevices().Devices {
if device.Type == "ascend" && device.Available {
ids = append(ids, strings.TrimPrefix(device.ID, "ascend:"))
}
}
slices.SortFunc(ids, func(a, b string) int {
left, _ := strconv.Atoi(a)
right, _ := strconv.Atoi(b)
return left - right
})
return ids
}
+34
View File
@@ -57,12 +57,31 @@ func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorR
serverDTO.Dir = path.Join(global.Dir.TensorRTLLMDir, item.Name)
serverDTO.Image = envs["IMAGE"]
serverDTO.Command = getCommand(item.Env)
var project map[string]interface{}
if yaml.Unmarshal([]byte(item.DockerCompose), &project) == nil {
services, _ := project["services"].(map[string]interface{})
if service, ok := services[item.Name].(map[string]interface{}); ok {
params := make(map[string]interface{}, len(envs))
for key, value := range envs {
params[key] = value
}
serverDTO.GPU = ReadModelGPU(service, params, "nvidia")
}
}
serverDTO.ExposedPorts, _ = loadComposeExposedPortsFromEnv(envs, "0.0.0.0", false)
composeByte, err := files.NewFileOp().GetContent(path.Join(global.Dir.TensorRTLLMDir, item.Name, "docker-compose.yml"))
if err == nil {
serverDTO.Environments, _ = getDockerComposeEnvironments(composeByte)
environments := make([]request.Environment, 0, len(serverDTO.Environments))
for _, item := range serverDTO.Environments {
if item.Key == "MODEL_PATH" || (serverDTO.GPU != nil && (item.Key == "NVIDIA_VISIBLE_DEVICES" || item.Key == "NVIDIA_DRIVER_CAPABILITIES")) {
continue
}
environments = append(environments, item)
}
serverDTO.Environments = environments
}
volumes, err := getDockerComposeVolumes(composeByte)
if err == nil {
@@ -133,6 +152,9 @@ func handleLLMParams(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) e
var environments []interface{}
environments = append(environments, fmt.Sprintf("MODEL_PATH=%s", create.ModelDir))
for _, e := range create.Environments {
if e.Key == "MODEL_PATH" {
continue
}
environments = append(environments, fmt.Sprintf("%s=%s", e.Key, e.Value))
}
serviceValue["environment"] = environments
@@ -148,6 +170,18 @@ func handleLLMParams(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) e
volumes = append(volumes, formatComposeVolume(volume.Source, volume.Target, volume.Mode))
}
serviceValue["volumes"] = volumes
gpuParams := map[string]interface{}{}
existingEnv, err := gotenv.Unmarshal(llm.Env)
if err != nil {
return err
}
for key, value := range existingEnv {
gpuParams[key] = value
}
gpuParams["MODEL_PATH"] = create.ModelDir
if err := ApplyModelGPU(serviceValue, create.GPU, gpuParams, "nvidia"); err != nil {
return err
}
services[llm.Name] = serviceValue
composeByte, err := yaml.Marshal(composeMap)
+1
View File
@@ -26,6 +26,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/domain/update", baseApi.UpdateBindDomain)
aiToolsRouter.GET("/gpu/load", baseApi.LoadGpuInfo)
aiToolsRouter.GET("/model/gpu/devices", baseApi.LoadModelGPUDevices)
aiToolsRouter.POST("/gpu/search", baseApi.LoadGPUMonitor)
aiToolsRouter.GET("/gpu/options", baseApi.GetCPUOptions)
aiToolsRouter.POST("/vllm/monitor/search", baseApi.LoadVLLMMonitor)
+14
View File
@@ -61,6 +61,13 @@ const (
AcceleratorMetricValuePattern = `^\s*([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))\s*(.*?)\s*$`
AscendVersionPattern = `(?i)\bVersion:\s*([^\s|]+)`
AscendMemoryPattern = `([0-9]+(?:\.[0-9]+)?)\s*/\s*([0-9]+(?:\.[0-9]+)?)`
NvidiaGPUUUIDPattern = `^GPU-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`
NvidiaGPUDeviceIDPattern = `^([0-9]+|GPU-[a-zA-Z0-9-]+|MIG-[a-zA-Z0-9/-]+)$`
IntelGPURenderNamePattern = `^renderD[0-9]+$`
IntelGPUDevicePathPattern = `^/dev/dri/renderD[0-9]+$`
AscendGPUDeviceNamePattern = `^davinci[0-9]+$`
AscendGPUDevicePathPattern = `^/dev/davinci[0-9]+$`
AscendGPUDeviceIDPattern = `^[0-9]+$`
)
var regexMap = make(map[string]*regexp.Regexp)
@@ -122,6 +129,13 @@ func Init() {
AcceleratorMetricValuePattern,
AscendVersionPattern,
AscendMemoryPattern,
NvidiaGPUUUIDPattern,
NvidiaGPUDeviceIDPattern,
IntelGPURenderNamePattern,
IntelGPUDevicePathPattern,
AscendGPUDeviceNamePattern,
AscendGPUDevicePathPattern,
AscendGPUDeviceIDPattern,
}
for _, pattern := range patterns {
+16
View File
@@ -1,6 +1,21 @@
import { ReqPage } from '.';
export namespace AI {
export interface ModelGPUDevice {
id: string;
name: string;
type: string;
uuid?: string;
available: boolean;
}
export interface ModelGPUConfig {
mode: 'all' | 'count' | 'devices';
count?: number;
error?: string;
enabled: boolean;
deviceIDs: string[];
capabilities: string[];
}
export interface ModelAccountSync {
id: number;
modelAccountBaseURLType: 'container' | 'localhost' | 'systemIP' | 'custom';
@@ -392,6 +407,7 @@ export namespace AI {
}
export interface TensorRTLLM {
gpu?: ModelGPUConfig;
id?: number;
name: string;
containerName: string;
+2
View File
@@ -137,6 +137,7 @@ export namespace App {
}
export interface AppInstall {
gpu?: AI.ModelGPUConfig;
syncModelAccount?: boolean;
modelAccountBaseURLType?: string;
modelAccountBaseURL?: string;
@@ -310,6 +311,7 @@ export namespace App {
}
export interface AppConfig {
gpu?: AI.ModelGPUConfig;
appKey: string;
name: string;
httpPort: number;
+4
View File
@@ -43,6 +43,10 @@ export const updateBindDomain = (req: AI.BindDomain) => {
return http.post(`/ai/domain/update`, req);
};
export const loadModelGPUDevices = () => {
return http.get<{ devices: AI.ModelGPUDevice[]; warnings: string[] }>('/ai/model/gpu/devices');
};
export const loadGPUInfo = () => {
return http.get<AI.Info>(`/ai/gpu/load`);
};
@@ -1,5 +1,5 @@
<template>
<el-form-item prop="advanced">
<el-form-item v-if="showToggle" prop="advanced">
<el-checkbox v-model="form.advanced" :label="$t('app.advanced')" size="large" />
</el-form-item>
<div v-if="form.advanced">
@@ -83,6 +83,7 @@ const props = withDefaults(
editCompose: boolean;
dockerCompose: string;
};
showToggle?: boolean;
showAllowPort?: boolean;
showSpecifyIP?: boolean;
showContainerName?: boolean;
@@ -93,6 +94,7 @@ const props = withDefaults(
autoLoadLimit?: boolean;
}>(),
{
showToggle: true,
showAllowPort: true,
showSpecifyIP: true,
showContainerName: true,
+20 -5
View File
@@ -1037,6 +1037,23 @@ const message = {
validateImageAvailabilityHelper: 'Generates one minimal image before saving and may incur charges',
},
model: {
gpuAcceleration: 'GPU acceleration',
gpuMode: 'Accelerator selection',
gpuAll: 'All accelerators',
gpuCount: 'Specify count',
gpuSpecific: 'Select devices',
gpuAllHelper: 'Use all available accelerators of this type on the current node.',
gpuCountHelper:
'Let the runtime allocate the requested number of accelerators without binding specific devices.',
gpuSpecificHelper: 'Manually select specific accelerators.',
gpuCountRequired: 'Enter an integer greater than 0',
gpuDevices: 'GPU devices',
gpuCapabilitiesHelper:
'GPU access is required; enable monitoring and management (utility) and computing support (compute) as needed.',
gpuSelectRequired: 'Select at least one available GPU device.',
gpuUnsupported: 'Unsupported',
gpuNotDetected: 'No supported accelerators detected',
gpuMissing: 'Unavailable',
modelCategory: 'Model category',
modelName: 'Model name',
modelArchitecture: 'Model type',
@@ -1075,7 +1092,7 @@ const message = {
container_conn_helper: 'Use this address for inter-container access or connection',
from_remote: 'This model was not downloaded via 1Panel, no related pull logs.',
no_logs: 'The pull logs for this model have been deleted and cannot be viewed.',
vllmVersionHelper: 'For FusionXpark GB 10 servers, please select the -cu130 version.',
vllmVersionHelper: 'For FusionXpark GB10 servers, please select the -cu130 version.',
vllmDeployProfile: 'Deployment model',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1734,8 +1751,8 @@ const message = {
prohibitedHelper: 'Prohibited: Concurrent process execution is not allowed',
migModeHelper: 'Used to create MIG instances, implementing physical GPU isolation at the user layer.',
migModeNA: 'Not Supported',
current: 'Real-time Monitoring',
history: 'Historical Records',
current: 'Real-time Status',
history: 'Monitoring Data',
notSupport: 'The current version or driver does not support displaying this parameter.',
},
mcp: {
@@ -2043,8 +2060,6 @@ const message = {
retry: 'Retry selected points',
environment: 'Environment',
plan: 'Frozen configuration',
clientHelp:
'The client container runs on the main node and sends requests to an existing service. It does not start inference or require a GPU.',
retryHelp: 'Only selected abnormal points are retried, with the same parameters and new seeds.',
secretHelp: 'API keys and headers are excluded from copies. Enter them again if needed.',
resultHelp:
+20 -5
View File
@@ -1039,6 +1039,23 @@ const message = {
validateImageAvailabilityHelper: 'Genera una imagen mínima antes de guardar y puede generar costes',
},
model: {
gpuAcceleration: 'Aceleración GPU',
gpuMode: 'Selección de aceleradores',
gpuAll: 'Todos los aceleradores',
gpuCount: 'Especificar cantidad',
gpuSpecific: 'Seleccionar dispositivos',
gpuAllHelper: 'Usar todos los aceleradores disponibles de este tipo en el nodo actual.',
gpuCountHelper:
'El entorno de ejecución asigna la cantidad solicitada sin vincular dispositivos específicos.',
gpuSpecificHelper: 'Seleccione manualmente los aceleradores específicos.',
gpuCountRequired: 'Introduzca un entero mayor que 0',
gpuDevices: 'Dispositivos GPU',
gpuCapabilitiesHelper:
'El acceso a la GPU es obligatorio; active la supervisión y gestión (utility) y el cálculo (compute) según sea necesario.',
gpuSelectRequired: 'Seleccione al menos un dispositivo GPU disponible.',
gpuUnsupported: 'No compatible',
gpuNotDetected: 'No se detectaron aceleradores compatibles',
gpuMissing: 'No disponible',
modelCategory: 'Categoría del modelo',
modelName: 'Nombre del modelo',
modelArchitecture: 'Tipo de modelo',
@@ -1077,7 +1094,7 @@ const message = {
container_conn_helper: 'Utilice esta dirección para el acceso o conexión entre contenedores',
from_remote: 'Este modelo no fue descargado vía 1Panel, no hay registros de descarga relacionados.',
no_logs: 'Los registros de descarga de este modelo han sido eliminados y no se pueden consultar.',
vllmVersionHelper: 'Para servidores FusionXpark GB 10, seleccione la versión -cu130.',
vllmVersionHelper: 'Para servidores FusionXpark GB10, seleccione la versión -cu130.',
vllmDeployProfile: 'Modelo de despliegue',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1745,8 +1762,8 @@ const message = {
migModeHelper:
'Se utiliza para crear instancias MIG, implementando aislamiento físico de GPU en la capa de usuario.',
migModeNA: 'No Compatible',
current: 'Monitoreo en Tiempo Real',
history: 'Registros Históricos',
current: 'Estado en tiempo real',
history: 'Datos de monitoreo',
notSupport: 'La versión actual o el controlador no admiten mostrar este parámetro.',
processCount: 'Procesos',
},
@@ -2061,8 +2078,6 @@ const message = {
retry: 'Repetir puntos seleccionados',
environment: 'Entorno',
plan: 'Configuración fijada',
clientHelp:
'El contenedor cliente se ejecuta en el nodo principal y envía solicitudes a un servicio existente. No inicia inferencia ni requiere GPU.',
retryHelp: 'Solo se repiten los puntos anómalos seleccionados, con los mismos parámetros y nuevas semillas.',
secretHelp: 'Las copias no incluyen claves API ni cabeceras. Vuelva a introducirlas si es necesario.',
resultHelp:
+19 -5
View File
@@ -1022,6 +1022,22 @@ const message = {
validateImageAvailabilityHelper: 'پیش از ذخیره یک تصویر حداقلی تولید می‌شود و ممکن است هزینه داشته باشد',
},
model: {
gpuAcceleration: 'شتاب‌دهی GPU',
gpuMode: 'انتخاب شتاب‌دهنده',
gpuAll: 'همه شتاب‌دهنده‌ها',
gpuCount: 'تعیین تعداد',
gpuSpecific: 'انتخاب دستگاه‌ها',
gpuAllHelper: 'از همه شتاب‌دهنده‌های موجود از این نوع در گره فعلی استفاده شود.',
gpuCountHelper: 'محیط اجرا تعداد درخواستی شتاب‌دهنده‌ها را بدون اتصال به دستگاه‌های مشخص تخصیص می‌دهد.',
gpuSpecificHelper: 'شتاب‌دهنده‌های مشخص را به‌صورت دستی انتخاب کنید.',
gpuCountRequired: 'یک عدد صحیح بزرگ‌تر از صفر وارد کنید',
gpuDevices: 'دستگاه‌های GPU',
gpuCapabilitiesHelper:
'دسترسی به GPU الزامی است؛ نظارت و مدیریت (utility) و پشتیبانی محاسباتی (compute) را در صورت نیاز فعال کنید.',
gpuSelectRequired: 'حداقل یک دستگاه GPU موجود را انتخاب کنید.',
gpuUnsupported: 'پشتیبانی نمی‌شود',
gpuNotDetected: 'هیچ شتاب‌دهنده پشتیبانی‌شده‌ای شناسایی نشد',
gpuMissing: 'در دسترس نیست',
modelCategory: 'دسته‌بندی مدل',
modelName: 'نام مدل',
modelArchitecture: 'نوع مدل',
@@ -1060,7 +1076,7 @@ const message = {
container_conn_helper: 'از این آدرس برای دسترسی بین کانتینرها یا اتصال استفاده کنید',
from_remote: 'این مدل از طریق 1Panel دانلود نشده است، لاگ مربوط به دریافت وجود ندارد.',
no_logs: 'لاگ دریافت این مدل حذف شده است و قابل مشاهده نیست.',
vllmVersionHelper: 'برای سرورهای FusionXpark GB 10، لطفاً نسخه -cu130 را انتخاب کنید.',
vllmVersionHelper: 'برای سرورهای FusionXpark GB10، لطفاً نسخه -cu130 را انتخاب کنید.',
vllmDeployProfile: 'مدل استقرار',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1717,8 +1733,8 @@ const message = {
migModeHelper:
'برای ایجاد نمونه‌های MIG استفاده می‌شود و ایزوله‌سازی فیزیکی GPU را در لایه کاربر پیاده‌سازی می‌کند.',
migModeNA: 'پشتیبانی نمی‌شود',
current: 'نظارت بلادرنگ',
history: 'سوابق تاریخی',
current: 'وضعیت بلادرنگ',
history: 'داده‌های نظارتی',
notSupport: 'نسخه فعلی یا درایور از نمایش این پارامتر پشتیبانی نمی‌کند.',
},
mcp: {
@@ -2021,8 +2037,6 @@ const message = {
retry: 'تکرار نقاط انتخاب‌شده',
environment: 'محیط اجرا',
plan: 'پیکربندی ثابت',
clientHelp:
'کانتینر کلاینت در گره اصلی به سرویس موجود درخواست می‌فرستد. سرویس استنتاج راه‌اندازی نمی‌کند و به GPU نیاز ندارد.',
retryHelp: 'فقط نقاط غیرعادی انتخاب‌شده با همان پارامترها و بذرهای جدید تکرار می‌شوند.',
secretHelp: 'کپی‌ها شامل کلید API و سرآیندها نیستند. در صورت نیاز دوباره وارد کنید.',
resultHelp:
+19 -5
View File
@@ -1026,6 +1026,22 @@ const message = {
validateImageAvailabilityHelper: '保存前に最小画像を1枚生成するため、料金が発生する場合があります',
},
model: {
gpuAcceleration: 'GPU アクセラレーション',
gpuMode: 'アクセラレーターの選択',
gpuAll: 'すべてのアクセラレーター',
gpuCount: '台数を指定',
gpuSpecific: 'デバイスを指定',
gpuAllHelper: '現在のノードで利用可能な同じ種類のアクセラレーターをすべて使用します。',
gpuCountHelper: '特定のデバイスに固定せず、ランタイムが指定台数を割り当てます。',
gpuSpecificHelper: '使用するアクセラレーターを手動で選択します。',
gpuCountRequired: '0 より大きい整数を入力してください',
gpuDevices: 'GPU デバイス',
gpuCapabilitiesHelper:
'GPU アクセスは必須です。必要に応じて監視と管理(utility)や計算サポート(compute)を有効にしてください。',
gpuSelectRequired: '使用可能な GPU を少なくとも 1 つ選択してください。',
gpuUnsupported: '非対応',
gpuNotDetected: '対応するアクセラレーターが検出されませんでした',
gpuMissing: '利用不可',
modelCategory: 'モデルカテゴリ',
modelName: 'モデル名',
modelArchitecture: 'モデルの種類',
@@ -1064,7 +1080,7 @@ const message = {
container_conn_helper: 'コンテナ間のアクセスまたは接続にこのアドレスを使用',
from_remote: 'このモデルは1Panelを介してダウンロードされておらず、関連するプルログはありません。',
no_logs: 'このモデルのプルログは削除されており、関連するログを表示できません。',
vllmVersionHelper: 'FusionXpark GB 10 サーバーでは -cu130 バージョンを選択してください。',
vllmVersionHelper: 'FusionXpark GB10 サーバーでは -cu130 バージョンを選択してください。',
vllmDeployProfile: 'デプロイモデル',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1717,8 +1733,8 @@ const message = {
prohibitedHelper: '禁止: プロセスの同時実行は許可されません',
migModeHelper: 'MIGインスタンスを作成するために使用され、ユーザーレイヤーでGPUの物理的隔離を実装します。',
migModeNA: 'サポートされていません',
current: 'リアルタイム監視',
history: '履歴記録',
current: 'リアルタイム状態',
history: '監視データ',
notSupport: '現在のバージョンまたはドライバーはこのパラメータの表示をサポートしていません。',
processCount: 'プロセス数',
},
@@ -2026,8 +2042,6 @@ const message = {
retry: '選択した測定点を再試行',
environment: '実行環境',
plan: '固定された設定',
clientHelp:
'クライアントコンテナはメインノードで既存サービスに要求を送信します。推論サービスの起動や GPU の割り当ては不要です。',
retryHelp: '選択した異常な測定点のみ、同じパラメータと新しいシードで再試行します。',
secretHelp: 'コピーに API キーとヘッダーは含まれません。必要に応じて再入力してください。',
resultHelp:
+19 -5
View File
@@ -1016,6 +1016,22 @@ const message = {
validateImageAvailabilityHelper: '저장 전에 최소 이미지 한 장을 생성하며 비용이 발생할 수 있습니다',
},
model: {
gpuAcceleration: 'GPU 가속',
gpuMode: '가속기 선택',
gpuAll: '모든 가속기',
gpuCount: '수량 지정',
gpuSpecific: '장치 선택',
gpuAllHelper: '현재 노드에서 사용 가능한 같은 유형의 가속기를 모두 사용합니다.',
gpuCountHelper: '특정 장치에 고정하지 않고 런타임이 지정한 수량의 가속기를 할당합니다.',
gpuSpecificHelper: '사용할 가속기를 직접 선택합니다.',
gpuCountRequired: '0보다 큰 정수를 입력하세요',
gpuDevices: 'GPU 장치',
gpuCapabilitiesHelper:
'GPU 접근은 필수이며, 필요에 따라 모니터링 및 관리(utility)와 연산 지원(compute)을 활성화할 수 있습니다.',
gpuSelectRequired: '사용 가능한 GPU 장치를 하나 이상 선택하세요.',
gpuUnsupported: '지원 안 됨',
gpuNotDetected: '지원되는 가속기가 감지되지 않았습니다',
gpuMissing: '사용 불가',
modelCategory: '모델 카테고리',
modelName: '모델 이름',
modelArchitecture: '모델 유형',
@@ -1054,7 +1070,7 @@ const message = {
container_conn_helper: '컨테이너 간 접근 또는 연결에 이 주소를 사용',
from_remote: '이 모델은 1Panel을 통해 다운로드되지 않았으며 관련 풀 로그가 없습니다.',
no_logs: '이 모델의 풀 로그가 삭제되어 관련 로그를 볼 수 없습니다.',
vllmVersionHelper: 'FusionXpark GB 10 서버는 -cu130 버전을 선택하세요.',
vllmVersionHelper: 'FusionXpark GB10 서버는 -cu130 버전을 선택하세요.',
vllmDeployProfile: '배포 모델',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1702,8 +1718,8 @@ const message = {
prohibitedHelper: '금지됨: 프로세스 동시 실행이 허용되지 않음',
migModeHelper: 'MIG 인스턴스를 생성하는 데 사용되며 사용자 레이어에서 GPU의 물리적 격리를 구현합니다.',
migModeNA: '지원되지 않음',
current: '실시간 모니터링',
history: '기록',
current: '실시간 상태',
history: '모니터링 데이터',
notSupport: '현재 버전 또는 드라이버는 이 매개변수 표시를 지원하지 않습니다.',
processCount: '프로세스 수',
},
@@ -2002,8 +2018,6 @@ const message = {
retry: '선택한 측정점 재시도',
environment: '실행 환경',
plan: '고정 설정',
clientHelp:
'클라이언트 컨테이너는 주 노드에서 기존 서비스에 요청을 보냅니다. 추론 서비스를 시작하거나 GPU를 할당하지 않습니다.',
retryHelp: '선택한 비정상 측정점만 같은 매개변수와 새 시드로 재시도합니다.',
secretHelp: '복사본에는 API 키와 헤더가 없습니다. 필요한 경우 다시 입력하세요.',
resultHelp:
+19 -5
View File
@@ -1013,6 +1013,22 @@ const message = {
validateImageAvailabilityHelper: 'ສ້າງຮູບຂະໜາດນ້ອຍໜຶ່ງຮູບກ່ອນບັນທຶກ ແລະ ອາດມີຄ່າໃຊ້ຈ່າຍ',
},
model: {
gpuAcceleration: 'ການເລັ່ງ GPU',
gpuMode: 'ເລືອກຕົວເລັ່ງ',
gpuAll: 'ຕົວເລັ່ງທັງໝົດ',
gpuCount: 'ລະບຸຈຳນວນ',
gpuSpecific: 'ເລືອກອຸປະກອນ',
gpuAllHelper: 'ໃຊ້ຕົວເລັ່ງປະເພດດຽວກັນທັງໝົດທີ່ມີໃນໂນດປັດຈຸບັນ.',
gpuCountHelper: 'ລະບົບຈະຈັດສັນຕົວເລັ່ງຕາມຈຳນວນທີ່ລະບຸ ໂດຍບໍ່ຜູກກັບອຸປະກອນສະເພາະ.',
gpuSpecificHelper: 'ເລືອກຕົວເລັ່ງທີ່ຕ້ອງການດ້ວຍຕົນເອງ.',
gpuCountRequired: 'ກະລຸນາປ້ອນຈຳນວນເຕັມທີ່ຫຼາຍກວ່າ 0',
gpuDevices: 'ອຸປະກອນ GPU',
gpuCapabilitiesHelper:
'ຕ້ອງເປີດການເຂົ້າເຖິງ GPU ແລະ ສາມາດເປີດການຕິດຕາມແລະຈັດການ (utility) ແລະ ການຄຳນວນ (compute) ໄດ້ຕາມຕ້ອງການ.',
gpuSelectRequired: 'ກະລຸນາເລືອກ GPU ທີ່ພ້ອມໃຊ້ຢ່າງໜ້ອຍໜຶ່ງອຸປະກອນ',
gpuUnsupported: 'ບໍ່ຮອງຮັບ',
gpuNotDetected: 'ບໍ່ພົບຕົວເລັ່ງທີ່ຮອງຮັບ',
gpuMissing: 'ບໍ່ພ້ອມໃຊ້',
modelCategory: 'ໝວດໝູ່ໂມເດວ',
modelName: 'ຊື່ໂມເດວ',
modelArchitecture: 'ປະເພດໂມເດວ',
@@ -1051,7 +1067,7 @@ const message = {
container_conn_helper: 'ໃຊ້ທີ່ຢູ່ນີ້ສຳລັບການເຂົ້າເຖິງ ຫຼື ເຊື່ອມຕໍ່ລະຫວ່າງຄອນເທນເນີ',
from_remote: 'ໂມເດວນີ້ບໍ່ໄດ້ດາວໂຫຼດຜ່ານ 1Panel, ບໍ່ມີລັອກການດຶງຂໍ້ມູນທີ່ກ່ຽວຂ້ອງ.',
no_logs: 'ລັອກການດຶງຂໍ້ມູນຂອງໂມເດວນີ້ຖືກລຶບແລ້ວ ແລະ ບໍ່ສາມາດເບິ່ງໄດ້.',
vllmVersionHelper: 'ສຳລັບເຊີເວີ FusionXpark GB 10, ກະລຸນາເລືອກເວີຊັນ -cu130.',
vllmVersionHelper: 'ສຳລັບເຊີເວີ FusionXpark GB10, ກະລຸນາເລືອກເວີຊັນ -cu130.',
vllmDeployProfile: 'ໂມເດວສຳລັບຕິດຕັ້ງ',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1694,8 +1710,8 @@ const message = {
prohibitedHelper: 'ຫ້າມ: ບໍ່ອະນຸຍາດໃຫ້ລັນໂປຣເຊສພ້ອມກັນ',
migModeHelper: 'ໃຊ້ສຳລັບສ້າງ MIG instances ເພື່ອແຍກສ່ວນ GPU ທາງກາຍະພາບ.',
migModeNA: 'ບໍ່ຮອງຮັບ',
current: 'ຕິດຕາມແບບ Real-time',
history: 'ບັນທຶກຍ້ອນຫຼັງ',
current: 'ສະຖານະແບບທັນທີ',
history: 'ຂໍ້ມູນການຕິດຕາມ',
notSupport: 'ເວີຊັນ ຫຼື driver ປັດຈຸບັນບໍ່ຮອງຮັບການສະແດງຄ່ານີ້.',
},
mcp: {
@@ -1991,8 +2007,6 @@ const message = {
retry: 'ທົດສອບຈຸດທີ່ເລືອກຄືນ',
environment: 'ສະພາບແວດລ້ອມ',
plan: 'ການຕັ້ງຄ່າຄົງທີ່',
clientHelp:
'ຄອນເທນເນີລູກຂ່າຍເຮັດວຽກຢູ່ໂນດຫຼັກ ແລະ ສົ່ງຄຳຮ້ອງຂໍໄປບໍລິການທີ່ມີແລ້ວ. ບໍ່ເລີ່ມບໍລິການອະນຸມານ ແລະ ບໍ່ຕ້ອງການ GPU.',
retryHelp: 'ທົດສອບຄືນສະເພາະຈຸດຜິດປົກກະຕິທີ່ເລືອກ ດ້ວຍພາລາມິເຕີເດີມ ແລະ ຄ່າສຸ່ມໃໝ່.',
secretHelp: 'ສຳເນົາບໍ່ມີລະຫັດ API ແລະ ສ່ວນຫົວ. ກະລຸນາປ້ອນໃໝ່ຖ້າຈຳເປັນ.',
resultHelp:
+19 -5
View File
@@ -1048,6 +1048,22 @@ const message = {
validateImageAvailabilityHelper: 'Menjana satu imej minimum sebelum menyimpan dan mungkin dikenakan caj',
},
model: {
gpuAcceleration: 'Pecutan GPU',
gpuMode: 'Pemilihan pemecut',
gpuAll: 'Semua pemecut',
gpuCount: 'Nyatakan bilangan',
gpuSpecific: 'Pilih peranti',
gpuAllHelper: 'Gunakan semua pemecut jenis ini yang tersedia pada nod semasa.',
gpuCountHelper: 'Masa jalan memperuntukkan bilangan pemecut yang diminta tanpa mengikat peranti tertentu.',
gpuSpecificHelper: 'Pilih pemecut tertentu secara manual.',
gpuCountRequired: 'Masukkan integer yang lebih besar daripada 0',
gpuDevices: 'Peranti GPU',
gpuCapabilitiesHelper:
'Akses GPU diperlukan; dayakan pemantauan dan pengurusan (utility) serta sokongan pengiraan (compute) mengikut keperluan.',
gpuSelectRequired: 'Pilih sekurang-kurangnya satu peranti GPU yang tersedia.',
gpuUnsupported: 'Tidak disokong',
gpuNotDetected: 'Tiada pemecut yang disokong dikesan',
gpuMissing: 'Tidak tersedia',
modelCategory: 'Kategori model',
modelName: 'Nama model',
modelArchitecture: 'Jenis model',
@@ -1086,7 +1102,7 @@ const message = {
container_conn_helper: 'Gunakan alamat ini untuk akses atau sambungan antara kontena',
from_remote: 'Model ini tidak dimuat turun melalui 1Panel; tiada log muat turun berkaitan.',
no_logs: 'Log muat turun model ini telah dipadam dan tidak boleh dilihat.',
vllmVersionHelper: 'Untuk pelayan FusionXpark GB 10, sila pilih versi -cu130.',
vllmVersionHelper: 'Untuk pelayan FusionXpark GB10, sila pilih versi -cu130.',
vllmDeployProfile: 'Model penggunaan',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1747,8 +1763,8 @@ const message = {
migModeHelper:
'Digunakan untuk mencipta contoh MIG, melaksanakan pengasingan fizikal GPU pada lapisan pengguna.',
migModeNA: 'Tidak Disokong',
current: 'Pemantauan Masa Nyata',
history: 'Rekod Sejarah',
current: 'Status Masa Nyata',
history: 'Data Pemantauan',
notSupport: 'Versi atau pemacu semasa tidak menyokong paparan parameter ini.',
processCount: 'Bilangan proses',
},
@@ -2058,8 +2074,6 @@ const message = {
retry: 'Ulang titik terpilih',
environment: 'Persekitaran',
plan: 'Konfigurasi dibekukan',
clientHelp:
'Kontena klien berjalan pada nod utama dan menghantar permintaan kepada perkhidmatan sedia ada. Ia tidak memulakan inferens atau memerlukan GPU.',
retryHelp: 'Hanya titik bermasalah yang dipilih diulang, dengan parameter sama dan benih baharu.',
secretHelp: 'Salinan tidak mengandungi kunci API atau pengepala. Masukkan semula jika perlu.',
resultHelp:
+20 -5
View File
@@ -1043,6 +1043,23 @@ const message = {
validateImageAvailabilityHelper: 'Gera uma imagem mínima antes de salvar e pode gerar custos',
},
model: {
gpuAcceleration: 'Aceleração de GPU',
gpuMode: 'Seleção de aceleradores',
gpuAll: 'Todos os aceleradores',
gpuCount: 'Especificar quantidade',
gpuSpecific: 'Selecionar dispositivos',
gpuAllHelper: 'Usar todos os aceleradores disponíveis deste tipo no nó atual.',
gpuCountHelper:
'O ambiente de execução aloca a quantidade solicitada sem vincular dispositivos específicos.',
gpuSpecificHelper: 'Selecione manualmente os aceleradores específicos.',
gpuCountRequired: 'Insira um número inteiro maior que 0',
gpuDevices: 'Dispositivos GPU',
gpuCapabilitiesHelper:
'O acesso à GPU é obrigatório; habilite o monitoramento e gerenciamento (utility) e o suporte a cálculos (compute) conforme necessário.',
gpuSelectRequired: 'Selecione pelo menos um dispositivo GPU disponível.',
gpuUnsupported: 'Não suportado',
gpuNotDetected: 'Nenhum acelerador compatível detectado',
gpuMissing: 'Indisponível',
modelCategory: 'Categoria do modelo',
modelName: 'Nome do modelo',
modelArchitecture: 'Tipo de modelo',
@@ -1081,7 +1098,7 @@ const message = {
container_conn_helper: 'Use este endereço para acesso ou conexão entre contêineres',
from_remote: 'Este modelo não foi baixado pelo 1Panel; não há logs de download relacionados.',
no_logs: 'Os logs de download deste modelo foram excluídos e não podem ser visualizados.',
vllmVersionHelper: 'Para servidores FusionXpark GB 10, selecione a versão -cu130.',
vllmVersionHelper: 'Para servidores FusionXpark GB10, selecione a versão -cu130.',
vllmDeployProfile: 'Modelo de implantação',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1746,8 +1763,8 @@ const message = {
migModeHelper:
'Usado para criar instâncias MIG, implementando isolamento físico da GPU na camada do usuário.',
migModeNA: 'Não Suportado',
current: 'Monitoramento em Tempo Real',
history: 'Registros Históricos',
current: 'Status em tempo real',
history: 'Dados de monitoramento',
notSupport: 'A versão atual ou o driver não suportam exibir este parâmetro.',
processCount: 'Qtd. de processos',
},
@@ -2059,8 +2076,6 @@ const message = {
retry: 'Repetir pontos selecionados',
environment: 'Ambiente',
plan: 'Configuração fixada',
clientHelp:
'O contêiner cliente roda no nó principal e envia requisições a um serviço existente. Não inicia inferência nem exige GPU.',
retryHelp: 'Apenas os pontos anormais selecionados são repetidos, com os mesmos parâmetros e novas sementes.',
secretHelp: 'As cópias não incluem chaves de API nem cabeçalhos. Informe-os novamente se necessário.',
resultHelp:
+20 -5
View File
@@ -1036,6 +1036,23 @@ const message = {
'Перед сохранением создаётся одно минимальное изображение; возможна оплата',
},
model: {
gpuAcceleration: 'Ускорение GPU',
gpuMode: 'Выбор ускорителей',
gpuAll: 'Все ускорители',
gpuCount: 'Указать количество',
gpuSpecific: 'Выбрать устройства',
gpuAllHelper: 'Использовать все доступные ускорители этого типа на текущем узле.',
gpuCountHelper:
'Среда выполнения выделяет указанное количество ускорителей без привязки к конкретным устройствам.',
gpuSpecificHelper: 'Выберите конкретные ускорители вручную.',
gpuCountRequired: 'Введите целое число больше 0',
gpuDevices: 'Устройства GPU',
gpuCapabilitiesHelper:
'Доступ к GPU обязателен; при необходимости включите мониторинг и управление (utility) и поддержку вычислений (compute).',
gpuSelectRequired: 'Выберите хотя бы одно доступное устройство GPU.',
gpuUnsupported: 'Не поддерживается',
gpuNotDetected: 'Поддерживаемые ускорители не обнаружены',
gpuMissing: 'Недоступно',
modelCategory: 'Категория модели',
modelName: 'Название модели',
modelArchitecture: 'Тип модели',
@@ -1074,7 +1091,7 @@ const message = {
container_conn_helper: 'Используйте этот адрес для доступа или подключения между контейнерами',
from_remote: 'Эта модель не была загружена через 1Panel, нет связанных журналов извлечения.',
no_logs: 'Журналы извлечения для этой модели были удалены и не могут быть просмотрены.',
vllmVersionHelper: 'Для серверов FusionXpark GB 10 выберите версию -cu130.',
vllmVersionHelper: 'Для серверов FusionXpark GB10 выберите версию -cu130.',
vllmDeployProfile: 'Модель развертывания',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1735,8 +1752,8 @@ const message = {
migModeHelper:
'Используется для создания экземпляров MIG, реализуя физическую изоляцию GPU на пользовательском уровне.',
migModeNA: 'Не Поддерживается',
current: 'Мониторинг в Реальном Времени',
history: 'Исторические Записи',
current: 'Состояние в реальном времени',
history: 'Данные мониторинга',
notSupport: 'Текущая версия или драйвер не поддерживает отображение этого параметра.',
processCount: 'Кол-во процессов',
},
@@ -2052,8 +2069,6 @@ const message = {
retry: 'Повторить выбранные точки',
environment: 'Среда выполнения',
plan: 'Зафиксированная конфигурация',
clientHelp:
'Клиентский контейнер работает на главном узле и отправляет запросы существующему сервису. Он не запускает модель и не требует GPU.',
retryHelp: 'Повторяются только выбранные проблемные точки с прежними параметрами и новыми начальными числами.',
secretHelp: 'Копии не содержат ключи API и заголовки. При необходимости введите их заново.',
resultHelp:
+19 -5
View File
@@ -1047,6 +1047,22 @@ const message = {
validateImageAvailabilityHelper: 'Kaydetmeden önce bir küçük görsel üretir ve ücret doğurabilir',
},
model: {
gpuAcceleration: 'GPU hızlandırma',
gpuMode: 'Hızlandırıcı seçimi',
gpuAll: 'Tüm hızlandırıcılar',
gpuCount: 'Sayı belirt',
gpuSpecific: 'Cihaz seç',
gpuAllHelper: 'Geçerli düğümde bu türdeki tüm kullanılabilir hızlandırıcıları kullanın.',
gpuCountHelper: 'Çalışma zamanı, belirli cihazlara bağlamadan istenen sayıda hızlandırıcı tahsis eder.',
gpuSpecificHelper: 'Belirli hızlandırıcıları elle seçin.',
gpuCountRequired: '0’dan büyük bir tam sayı girin',
gpuDevices: 'GPU aygıtları',
gpuCapabilitiesHelper:
'GPU erişimi zorunludur; izleme ve yönetimi (utility) ve hesaplama desteğini (compute) gerektiğinde etkinleştirin.',
gpuSelectRequired: 'En az bir kullanılabilir GPU aygıtı seçin.',
gpuUnsupported: 'Desteklenmiyor',
gpuNotDetected: 'Desteklenen hızlandırıcı algılanmadı',
gpuMissing: 'Kullanılamıyor',
modelCategory: 'Model kategorisi',
modelName: 'Model adı',
modelArchitecture: 'Model türü',
@@ -1085,7 +1101,7 @@ const message = {
container_conn_helper: 'Konteynerler arası erişim veya bağlantı için bu adresi kullanın',
from_remote: 'Bu model 1Panel aracılığıyla indirilmedi, ilgili çekme logları yok.',
no_logs: 'Bu modelin çekme logları silindi ve görüntülenemiyor.',
vllmVersionHelper: 'FusionXpark GB 10 sunucuları için lütfen -cu130 sürümünü seçin.',
vllmVersionHelper: 'FusionXpark GB10 sunucuları için lütfen -cu130 sürümünü seçin.',
vllmDeployProfile: 'Dağıtım modeli',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1743,8 +1759,8 @@ const message = {
migModeHelper:
"MIG örnekleri oluşturmak için kullanılır, GPU'nun kullanıcı katmanında fiziksel izolasyonunu uygular.",
migModeNA: 'Desteklenmiyor',
current: 'Gerçek Zamanlı İzleme',
history: 'Geçmiş Kayıtlar',
current: 'Gerçek Zamanlı Durum',
history: 'İzleme Verileri',
notSupport: 'Mevcut sürüm veya sürücü bu parametrenin görüntülenmesini desteklemiyor.',
processCount: 'Süreç sayısı',
},
@@ -2058,8 +2074,6 @@ const message = {
retry: 'Seçili ölçümleri yinele',
environment: 'Çalışma ortamı',
plan: 'Sabitlenmiş yapılandırma',
clientHelp:
'İstemci konteyneri ana düğümde çalışır ve mevcut hizmete istek gönderir. Çıkarım hizmeti başlatmaz ve GPU gerektirmez.',
retryHelp: 'Yalnızca seçili sorunlu ölçümler aynı parametreler ve yeni tohumlarla yinelenir.',
secretHelp: 'Kopyalar API anahtarlarını ve başlıkları içermez. Gerekiyorsa yeniden girin.',
resultHelp:
+18 -4
View File
@@ -977,6 +977,21 @@ const message = {
validateImageAvailabilityHelper: '儲存前會實際產生一張最小圖片,可能產生費用',
},
model: {
gpuAcceleration: 'GPU 加速',
gpuMode: '加速卡選擇',
gpuAll: '全部加速卡',
gpuCount: '指定數量',
gpuSpecific: '指定裝置',
gpuAllHelper: '容器使用目前節點所有可用的同類型加速卡。',
gpuCountHelper: '由執行環境分配指定數量的加速卡,不綁定特定裝置。',
gpuSpecificHelper: '手動選擇特定加速卡。',
gpuCountRequired: '請輸入大於 0 的整數',
gpuDevices: 'GPU 裝置',
gpuCapabilitiesHelper: 'GPU 存取為必選項,可依需求啟用監控管理(utility)和運算支援(compute)。',
gpuSelectRequired: '請選擇至少一個目前可用的 GPU 裝置',
gpuUnsupported: '不支援',
gpuNotDetected: '未偵測到可用加速卡',
gpuMissing: '無法使用',
modelCategory: '模型類別',
modelName: '模型名稱',
modelArchitecture: '模型類型',
@@ -1015,7 +1030,7 @@ const message = {
container_conn_helper: '容器間瀏覽或連接使用此地址',
from_remote: '該模型並非透過 1Panel 下載,無相關拉取日誌。',
no_logs: '該模型的拉取日誌已被刪除,無法檢視相關日誌。',
vllmVersionHelper: 'FusionXpark GB 10 伺服器請選擇 -cu130 版本',
vllmVersionHelper: 'FusionXpark GB10 伺服器請選擇 -cu130 版本',
vllmDeployProfile: '部署模型',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1643,8 +1658,8 @@ const message = {
prohibitedHelper: '禁止: 不允許行程同時執行',
migModeHelper: '用於建立 MIG 實例,在使用者層實現 GPU 的物理隔離。',
migModeNA: '不支援',
current: '即時監控',
history: '歷史記錄',
current: '即時狀態',
history: '監控資料',
notSupport: '當前版本或驅動不支援顯示該參數。',
processCount: '行程數',
},
@@ -1928,7 +1943,6 @@ const message = {
retry: '重測選取測點',
environment: '執行環境',
plan: '凍結設定',
clientHelp: '壓測容器在主節點執行,向既有服務傳送請求。不會啟動推論服務,也不需分配 GPU。',
retryHelp: '本輪只重測選取的異常測點,保留參數並重新分配隨機種子。',
secretHelp: '複製設定不包含 API Key 與請求標頭,請視需要重新填寫。',
resultHelp:
+18 -4
View File
@@ -990,6 +990,21 @@ const message = {
validateImageAvailabilityHelper: '保存前会真实生成一张最小图片,可能产生费用',
},
model: {
gpuAcceleration: 'GPU 加速',
gpuMode: '加速卡选择',
gpuAll: '全部加速卡',
gpuCount: '指定数量',
gpuSpecific: '指定设备',
gpuAllHelper: '容器使用当前节点所有可用的同类型加速卡。',
gpuCountHelper: '由运行时分配指定数量的加速卡,不绑定具体设备。',
gpuSpecificHelper: '手动选择具体加速卡。',
gpuCountRequired: '请输入大于 0 的整数',
gpuDevices: 'GPU 设备',
gpuCapabilitiesHelper: 'GPU 访问为必选项,可按需启用监控管理(utility)和计算支持(compute)。',
gpuSelectRequired: '请选择至少一个当前可用的 GPU 设备',
gpuUnsupported: '不支持',
gpuNotDetected: '未检测到可用加速卡',
gpuMissing: '不可用',
modelCategory: '模型类别',
modelName: '模型名称',
modelArchitecture: '模型类型',
@@ -1028,7 +1043,7 @@ const message = {
container_conn_helper: '容器间访问或连接使用此地址',
from_remote: '该模型并非通过 1Panel 下载,无相关拉取日志。',
no_logs: '该模型的拉取日志已被删除,无法查看相关日志。',
vllmVersionHelper: 'FusionXpark GB 10 服务器请选择 -cu130 版本',
vllmVersionHelper: 'FusionXpark GB10 服务器请选择 -cu130 版本',
vllmDeployProfile: '部署模型',
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
vllmProfileVisionExp: 'DeepSeek V4 Flash Vision Exp',
@@ -1658,8 +1673,8 @@ const message = {
prohibitedHelper: '禁止: 不允许进程同时执行',
migModeHelper: '用于创建 MIG 实例,在用户层实现 GPU 的物理隔离。',
migModeNA: '不支持',
current: '实时监控',
history: '历史记录',
current: '实时状态',
history: '监控数据',
notSupport: '当前版本或驱动不支持显示该参数。',
},
mcp: {
@@ -1936,7 +1951,6 @@ const message = {
retry: '补测选中测点',
environment: '执行环境',
plan: '冻结配置',
clientHelp: '压测容器在主节点运行,向已有服务发送请求。不会启动推理服务,也不需要分配 GPU。',
retryHelp: '本轮只补测选中的异常测点,保留参数并重新分配随机种子。',
secretHelp: '复制配置不包含 API Key 和请求头,请按需重新填写。',
resultHelp:
@@ -1,5 +1,5 @@
<template>
<el-form-item v-if="showSync">
<el-form-item v-if="showSync && !hideSyncToggle">
<el-checkbox v-model="form.syncModelAccount">{{ $t('aiTools.model.syncModelAccount') }}</el-checkbox>
</el-form-item>
<template v-if="!showSync || form.syncModelAccount">
@@ -52,6 +52,7 @@ const props = defineProps<{
containerPort: string;
hostMode?: boolean;
showSync?: boolean;
hideSyncToggle?: boolean;
}>();
const systemIP = ref('');
const addressTypes = computed(() => [
@@ -57,13 +57,17 @@
v-if="visible && view && view !== VllmPlaceholder && !failed && !missing"
ref="contentRef"
v-bind="
view === EngineConnection || view === AppParams
? {}
: view === ContainerLogs
? { highlightDiff: 210 }
: view === TaskLog
? { width: '70%' }
: { dialog: true }
view === AppParams
? { modelGPU: true }
: view === EngineConnection
? {}
: view === ContainerLogs
? { highlightDiff: 210 }
: view === TaskLog
? { width: '70%' }
: actionName === 'config'
? {}
: { dialog: true }
"
@close="returnToManagement"
@search="emit('search')"
@@ -0,0 +1,41 @@
.form-section {
margin-bottom: 12px;
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
background: var(--el-bg-color-overlay);
}
.form-section-header {
display: flex;
align-items: center;
gap: 8px;
min-height: 40px;
padding: 6px 12px;
color: var(--el-text-color-regular);
font-size: 13px;
font-weight: 600;
line-height: 1.5;
cursor: pointer;
list-style: none;
&::-webkit-details-marker {
display: none;
}
&::after {
content: '';
width: 6px;
height: 6px;
margin-left: auto;
flex-shrink: 0;
border-right: 1px solid currentColor;
border-bottom: 1px solid currentColor;
transform: rotate(-45deg);
}
}
.form-section[open] > .form-section-header::after {
transform: rotate(45deg);
}
.form-section-body {
padding: 12px;
border-top: 1px solid var(--el-border-color-light);
}
@@ -0,0 +1,7 @@
import type { FormInstance } from 'element-plus';
export const expandInvalidFormSection = (form: FormInstance, prop: string | string[], valid: boolean) => {
if (valid) return;
const section = form?.getField(prop)?.$el?.closest<HTMLDetailsElement>('details.form-section');
if (section) section.open = true;
};
@@ -0,0 +1,246 @@
<template>
<details open class="form-section">
<summary class="form-section-header">
<span>{{ $t('aiTools.model.gpuAcceleration') }}</span>
<el-switch
@click.stop
@keydown.stop
v-model="model.enabled"
:disabled="loading"
:aria-label="$t('aiTools.model.gpuAcceleration')"
@change="changeEnabled"
/>
<el-text v-if="!loading && !hasSupportedGPU" type="info" size="small">
{{ $t(gpuDevices ? 'aiTools.model.gpuNotDetected' : 'aiTools.model.loadFailed') }}
</el-text>
</summary>
<div v-if="model.enabled || model.error" v-show="!loading" class="form-section-body">
<el-form-item
prop="gpu.mode"
:rules="gpuModeRules"
:error="model.error"
:label="model.enabled ? $t('aiTools.model.gpuMode') : undefined"
>
<el-radio-group v-if="model.enabled" v-model="model.mode" @change="changeMode">
<el-radio-button value="all">
{{ $t('aiTools.model.gpuAll') }}
</el-radio-button>
<el-radio-button v-if="imageType === 'nvidia'" value="count">
{{ $t('aiTools.model.gpuCount') }}
</el-radio-button>
<el-radio-button value="devices">
{{ $t('aiTools.model.gpuSpecific') }}
</el-radio-button>
</el-radio-group>
<span v-if="model.enabled && model.mode === 'all'" class="input-help">
{{ $t('aiTools.model.gpuAllHelper') }}
</span>
<span v-if="model.enabled && model.mode === 'count'" class="input-help">
{{ $t('aiTools.model.gpuCountHelper') }}
</span>
<span v-if="model.enabled && model.mode === 'devices'" class="input-help">
{{ $t('aiTools.model.gpuSpecificHelper') }}
</span>
</el-form-item>
<template v-if="model.enabled">
<el-form-item
v-if="model.mode === 'count'"
:label="$t('aiTools.model.gpuCount')"
prop="gpu.count"
:rules="gpuCountRules"
>
<el-input-number
v-model="model.count"
:min="1"
:precision="0"
:step="1"
step-strictly
@change="model.error = undefined"
/>
</el-form-item>
<el-form-item
v-if="model.mode === 'devices'"
:label="$t('aiTools.model.gpuDevices')"
prop="gpu.deviceIDs"
:rules="gpuDeviceRules"
>
<el-select
v-model="model.deviceIDs"
@change="model.error = undefined"
popper-class="model-gpu-device-select"
multiple
filterable
class="w-full"
>
<el-option
v-for="device in gpuDevices || []"
:key="device.id"
:value="device.id"
:label="device.name"
:disabled="device.type !== imageType || !device.available"
>
<div class="flex items-center justify-between gap-2">
<span class="truncate">{{ device.name }}</span>
<span class="flex shrink-0 items-center gap-1">
<el-tag v-if="device.type !== imageType" size="small" type="warning">
{{ $t('aiTools.model.gpuUnsupported') }}
</el-tag>
<el-tag v-else-if="!device.available" size="small" type="danger">
{{ $t('aiTools.model.gpuMissing') }}
</el-tag>
<el-tag size="small" type="info">
{{ gpuTypeLabels[device.type] || device.type.toUpperCase() }}
</el-tag>
</span>
</div>
</el-option>
<el-option
v-for="id in missingGPUDevices"
:key="id"
:value="id"
:label="`${id} (${$t('aiTools.model.gpuMissing')})`"
disabled
/>
</el-select>
</el-form-item>
<el-form-item v-if="imageType === 'nvidia'" label="Capabilities" prop="gpu.capabilities">
<el-checkbox-group v-model="model.capabilities">
<el-checkbox value="gpu" disabled>gpu</el-checkbox>
<el-checkbox value="utility">utility</el-checkbox>
<el-checkbox value="compute">compute</el-checkbox>
</el-checkbox-group>
<span class="input-help">{{ $t('aiTools.model.gpuCapabilitiesHelper') }}</span>
</el-form-item>
</template>
<span v-if="selectingDevices && gpuWarnings.length" class="input-help">{{ gpuWarnings.join('; ') }}</span>
</div>
</details>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { AI } from '@/api/interface/ai';
import { loadModelGPUDevices } from '@/api/modules/ai';
import i18n from '@/lang';
const gpuTypeLabels: Record<string, string> = { nvidia: 'NVIDIA', intel: 'Intel', ascend: 'Ascend', amd: 'AMD' };
const props = withDefaults(defineProps<{ imageType?: string }>(), { imageType: 'nvidia' });
const model = defineModel<AI.ModelGPUConfig>({ required: true });
const loading = ref(false);
const gpuWarnings = ref<string[]>([]);
const gpuDevices = ref<AI.ModelGPUDevice[]>();
const hasSupportedGPU = computed(() =>
Boolean(gpuDevices.value?.some((item) => item.type === props.imageType && item.available)),
);
const selectingDevices = computed(() => model.value.enabled && model.value.mode === 'devices');
const missingGPUDevices = computed(() =>
model.value.deviceIDs.filter((id) => !(gpuDevices.value || []).some((item) => item.id === id)),
);
const gpuModeRules = [
{
validator: (_rule: unknown, _value: string, callback: (error?: Error) => void) => {
if (model.value.error) {
callback(new Error(model.value.error));
} else if (model.value.enabled && !gpuDevices.value) {
callback(new Error(i18n.global.t('aiTools.model.loadFailed')));
} else if (model.value.enabled && !hasSupportedGPU.value) {
callback(new Error(i18n.global.t('aiTools.model.gpuNotDetected')));
} else if (
model.value.enabled &&
(!['all', 'count', 'devices'].includes(model.value.mode) ||
(model.value.mode === 'count' && props.imageType !== 'nvidia'))
) {
callback(new Error(i18n.global.t('aiTools.model.gpuSelectRequired')));
} else {
callback();
}
},
trigger: 'change',
},
];
const gpuCountRules = [
{
validator: (_rule: unknown, value: number, callback: (error?: Error) => void) => {
const availableCount =
gpuDevices.value?.filter((item) => item.type === props.imageType && item.available).length || 0;
if (model.value.enabled && model.value.mode === 'count' && (!Number.isInteger(value) || value < 1)) {
callback(new Error(i18n.global.t('aiTools.model.gpuCountRequired')));
} else if (
model.value.enabled &&
model.value.mode === 'count' &&
availableCount > 0 &&
value > availableCount
) {
callback(new Error(i18n.global.t('commons.rule.numberRange', [1, availableCount])));
} else {
callback();
}
},
trigger: 'change',
},
];
const gpuDeviceRules = [
{
validator: (_rule: unknown, value: string[], callback: (error?: Error) => void) => {
if (selectingDevices.value && !value.length) {
callback(new Error(i18n.global.t('aiTools.model.gpuSelectRequired')));
} else {
callback();
}
},
trigger: 'change',
},
];
const changeMode = () => {
model.value.error = undefined;
model.value.deviceIDs = [];
model.value.count = model.value.mode === 'count' ? 1 : undefined;
};
const changeEnabled = () => {
model.value.error = undefined;
if (!model.value.enabled) {
model.value.mode = 'all';
model.value.count = undefined;
model.value.deviceIDs = [];
}
};
let requestVersion = 0;
const init = async (create: boolean) => {
const version = ++requestVersion;
loading.value = true;
gpuDevices.value = undefined;
gpuWarnings.value = [];
if (create) {
model.value.enabled = false;
model.value.mode = 'all';
model.value.count = undefined;
model.value.deviceIDs = [];
model.value.error = undefined;
}
try {
const { data } = await loadModelGPUDevices();
if (version !== requestVersion) return;
gpuDevices.value = data.devices || [];
gpuWarnings.value = data.warnings || [];
if (create) model.value.enabled = hasSupportedGPU.value;
} catch {
} finally {
if (version === requestVersion) {
loading.value = false;
}
}
};
defineExpose({ init, loading });
</script>
<style lang="scss" scoped>
@use '@/views/ai/model/components/form-section.scss';
:global(.model-gpu-device-select .el-select-dropdown__item.is-selected::after) {
display: none;
}
</style>
-10
View File
@@ -80,13 +80,6 @@
/>
</template>
<template #main>
<el-alert
v-if="loadFailed"
type="error"
:closable="false"
:title="$t('aiTools.model.loadFailed')"
class="mb-4"
/>
<ComplexTable
v-if="!isLocal"
:data="connections"
@@ -381,7 +374,6 @@ const poolRef = ref();
const terminalRef = ref<InstanceType<typeof TerminalDialog>>();
const syncRef = ref<InstanceType<typeof SyncService>>();
const loading = ref(false);
const loadFailed = ref(false);
const connections = ref<AI.AgentAccountItem[]>([]);
const accountColumns = ref<FuTableColumnConfig[]>([]);
const providers = ref<AI.ProviderInfo[]>([]);
@@ -507,7 +499,6 @@ const search = async () => {
const version = ++searchVersion;
if (!isCurrentPage.value || isFiles || isManagement.value) return;
loading.value = true;
loadFailed.value = false;
try {
if (isLocal) {
const data = await loadModelOverview(isProductPro.value, isFxplay.value);
@@ -533,7 +524,6 @@ const search = async () => {
connections.value = [];
pagination.total = 0;
deployments.value = [];
loadFailed.value = true;
} finally {
if (version === searchVersion) loading.value = false;
}
@@ -5,15 +5,30 @@
@update:model-value="open = $event"
@close="handleClose"
>
<AppInstallForm ref="formRef" v-model="form" :loading="loading">
<AppInstallForm ref="formRef" v-model="form" :loading="loading" sectioned hide-gpu-config>
<template #after-params>
<AccountAddress
ref="accountRef"
show-sync
:form="form"
:host-port="Number(form.params.PANEL_APP_PORT_HTTP) || 11434"
container-port="11434"
/>
<GPUConfig ref="gpuRef" v-model="form.gpu" />
<details open class="form-section">
<summary class="form-section-header">
<span>{{ $t('aiTools.model.syncModelAccount') }}</span>
<el-switch
@click.stop
@keydown.stop
v-model="form.syncModelAccount"
:aria-label="$t('aiTools.model.syncModelAccount')"
/>
</summary>
<div v-show="form.syncModelAccount" class="form-section-body">
<AccountAddress
ref="accountRef"
show-sync
hide-sync-toggle
:form="form"
:host-port="Number(form.params.PANEL_APP_PORT_HTTP) || 11434"
container-port="11434"
/>
</div>
</details>
</template>
</AppInstallForm>
<template v-if="!inline" #footer>
@@ -22,7 +37,7 @@
v-permission
type="primary"
:loading="loading"
:disabled="loading || !form.appDetailId"
:disabled="loading || gpuRef?.loading || !form.appDetailId"
@click="submit"
>
{{ $t('commons.button.confirm') }}
@@ -40,6 +55,8 @@ import i18n from '@/lang';
import DrawerPro from '@/components/drawer-pro/index.vue';
import AppInstallForm from '@/views/app-store/detail/form/index.vue';
import AccountAddress from '../../components/account-address/index.vue';
import { AI } from '@/api/interface/ai';
import GPUConfig from '../../components/gpu-config/index.vue';
const props = defineProps({ inline: Boolean });
const emit = defineEmits(['search', 'task', 'close']);
@@ -47,7 +64,14 @@ const open = ref(false);
const loading = ref(false);
const formRef = ref<InstanceType<typeof AppInstallForm>>();
const accountRef = ref();
const gpuRef = ref<InstanceType<typeof GPUConfig>>();
const form = ref({
gpu: {
enabled: true,
mode: 'all',
deviceIDs: [],
capabilities: ['gpu', 'utility', 'compute'],
} as AI.ModelGPUConfig,
appDetailId: 0,
name: '',
params: {} as Record<string, any>,
@@ -69,12 +93,15 @@ const openCreate = async () => {
if (!open.value) return;
formRef.value.resetForm();
formRef.value.setFormData({
gpu: { enabled: true, mode: 'all', deviceIDs: [], capabilities: ['gpu', 'utility', 'compute'] },
restartPolicy: 'unless-stopped',
syncModelAccount: true,
modelAccountBaseURLType: 'container',
modelAccountBaseURL: '',
});
await formRef.value.initForm('ollama', true);
if (!open.value) return;
await gpuRef.value?.init(true);
await nextTick();
formRef.value?.clearValidate();
} finally {
@@ -82,7 +109,7 @@ const openCreate = async () => {
}
};
const submit = async () => {
if (!form.value.appDetailId || !(await formRef.value?.validate())) return;
if (loading.value || gpuRef.value?.loading || !form.value.appDetailId || !(await formRef.value?.validate())) return;
const data = formRef.value.getFormData();
if (data.editCompose && !data.dockerCompose.trim()) {
MsgError(i18n.global.t('app.composeNullErr'));
@@ -112,5 +139,14 @@ watch(
watch(open, (value) => {
if (props.inline && !value) handleClose();
});
defineExpose({ openCreate, submit, loading, disabled: computed(() => !form.value.appDetailId) });
defineExpose({
openCreate,
submit,
loading,
disabled: computed(() => !form.value.appDetailId || gpuRef.value?.loading),
});
</script>
<style lang="scss" scoped>
@use '@/views/ai/model/components/form-section.scss';
</style>
@@ -1,66 +1,84 @@
<template>
<component
:is="inline ? 'div' : dialog ? DialogPro : DrawerPro"
:is="inline ? 'div' : DrawerPro"
v-bind="
inline
? {}
: {
modelValue: drawerVisiable,
header: $t('commons.button.' + mode),
title: $t('commons.button.' + mode),
size: 'large',
size: isMobile ? 'full' : 'large',
}
"
@update:model-value="drawerVisiable = $event"
@close="handleClose"
>
<el-alert :title="$t('aiTools.tensorRT.imageAlert')" class="common-prompt" :closable="false" type="warning" />
<el-form ref="formRef" label-position="top" :model="tensorRTLLM" :rules="rules" v-loading="loading">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input clearable v-model.trim="tensorRTLLM.name" :disabled="mode == 'edit'" />
</el-form-item>
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input v-model.trim="tensorRTLLM.containerName"></el-input>
</el-form-item>
<el-form-item :label="$t('container.image')" prop="image">
<el-input v-model.trim="tensorRTLLM.image" />
</el-form-item>
<el-form-item :label="$t('app.version')" prop="version">
<el-input v-model.trim="tensorRTLLM.version" />
</el-form-item>
<div v-if="isFxplay">
<el-form-item :label="$t('aiTools.tensorRT.modelSpeedup')" prop="modelSpeedup">
<el-switch v-model="tensorRTLLM.modelSpeedup" @change="changeModelSpeedup"></el-switch>
</el-form-item>
<el-form-item
:label="$t('aiTools.tensorRT.modelType')"
prop="modelType"
v-if="tensorRTLLM.modelSpeedup"
>
<el-select v-model="tensorRTLLM.modelType">
<el-option label="Qwen3" value="Qwen3" />
</el-select>
</el-form-item>
</div>
<el-form-item :label="$t('aiTools.tensorRT.modelDir')" prop="modelDir">
<el-input v-model="tensorRTLLM.modelDir">
<template #prepend>
<el-button icon="Folder" @click="modelDirRef.acceptParams({ dir: true })" />
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('runtime.runScript')" prop="command">
<el-input v-model="tensorRTLLM.command"></el-input>
<span class="input-help">
{{ $t('aiTools.tensorRT.commandHelper') }}
</span>
</el-form-item>
<NodeConfig v-model="tensorRTLLM" />
<el-form
@validate="(prop, valid) => expandInvalidFormSection(formRef, prop, valid)"
scroll-to-error
ref="formRef"
label-position="top"
:model="tensorRTLLM"
:rules="rules"
v-loading="loading"
>
<details open class="form-section">
<summary class="form-section-header">{{ $t('home.baseInfo') }}</summary>
<div class="form-section-body">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input clearable v-model.trim="tensorRTLLM.name" :disabled="mode == 'edit'" />
</el-form-item>
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input v-model.trim="tensorRTLLM.containerName"></el-input>
</el-form-item>
<el-form-item :label="$t('container.image')" prop="image">
<el-input v-model.trim="tensorRTLLM.image" />
</el-form-item>
<el-form-item :label="$t('app.version')" prop="version">
<el-input v-model.trim="tensorRTLLM.version" />
</el-form-item>
<div v-if="isFxplay">
<el-form-item :label="$t('aiTools.tensorRT.modelSpeedup')" prop="modelSpeedup">
<el-switch v-model="tensorRTLLM.modelSpeedup" @change="changeModelSpeedup"></el-switch>
</el-form-item>
<el-form-item
:label="$t('aiTools.tensorRT.modelType')"
prop="modelType"
v-if="tensorRTLLM.modelSpeedup"
>
<el-select v-model="tensorRTLLM.modelType">
<el-option label="Qwen3" value="Qwen3" />
</el-select>
</el-form-item>
</div>
<el-form-item :label="$t('aiTools.tensorRT.modelDir')" prop="modelDir">
<el-input v-model="tensorRTLLM.modelDir">
<template #prepend>
<el-button icon="Folder" @click="modelDirRef.acceptParams({ dir: true })" />
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('runtime.runScript')" prop="command">
<el-input v-model="tensorRTLLM.command"></el-input>
<span class="input-help">
{{ $t('aiTools.tensorRT.commandHelper') }}
</span>
</el-form-item>
</div>
</details>
<GPUConfig ref="gpuRef" v-model="tensorRTLLM.gpu" />
<details open class="form-section">
<summary class="form-section-header">{{ $t('app.advanced') }}</summary>
<div class="form-section-body">
<NodeConfig v-model="tensorRTLLM" />
</div>
</details>
</el-form>
<template v-if="!inline" #footer>
<span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button v-permission :disabled="loading" type="primary" @click="onSubmit">
<el-button v-permission :disabled="loading || gpuRef?.loading" type="primary" @click="onSubmit">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@@ -70,26 +88,35 @@
</template>
<script lang="ts" setup>
import DialogPro from '@/components/dialog-pro/index.vue';
const props = defineProps({ dialog: Boolean, inline: Boolean });
import { expandInvalidFormSection } from '@/views/ai/model/components/form-section';
const props = defineProps({ inline: Boolean });
import NodeConfig from '@/views/website/runtime//components/node-config.vue';
import DrawerPro from '@/components/drawer-pro/index.vue';
import FileList from '@/components/file-list/index.vue';
import { reactive, ref } from 'vue';
import { computed, nextTick, reactive, ref } from 'vue';
import { AI } from '@/api/interface/ai';
import GPUConfig from '../../components/gpu-config/index.vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, FormInstance } from 'element-plus';
import { createTensorRTLLM, updateTensorRTLLM } from '@/api/modules/ai';
import { MsgSuccess } from '@/utils/message';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isFxplay } = useGlobalStore();
const { isFxplay, isMobile } = useGlobalStore();
const loading = ref(false);
const gpuRef = ref<InstanceType<typeof GPUConfig>>();
const mode = ref('create');
const drawerVisiable = ref(false);
const newTensorRTLLM = () => {
return {
gpu: {
enabled: true,
mode: 'all',
deviceIDs: [],
capabilities: ['gpu', 'utility', 'compute'],
} as AI.ModelGPUConfig,
name: '',
containerName: '',
version: '1.2.0rc0',
@@ -108,15 +135,32 @@ const modelDirRef = ref();
const tensorRTLLM = ref(newTensorRTLLM());
const emit = defineEmits(['search', 'close']);
const openCreate = (): void => {
const openCreate = async () => {
mode.value = 'create';
drawerVisiable.value = true;
tensorRTLLM.value = newTensorRTLLM();
loading.value = true;
try {
await nextTick();
await gpuRef.value?.init(true);
} finally {
loading.value = false;
}
};
const openEdit = (rowData: any): void => {
const openEdit = async (rowData: any) => {
mode.value = 'edit';
tensorRTLLM.value = { ...rowData };
tensorRTLLM.value = {
...rowData,
gpu: {
...rowData.gpu,
mode: rowData.gpu?.mode || 'devices',
error: rowData.gpu ? rowData.gpu.error : i18n.global.t('aiTools.model.loadFailed'),
enabled: rowData.gpu?.enabled ?? false,
deviceIDs: [...(rowData.gpu?.deviceIDs || [])],
capabilities: [...(rowData.gpu?.capabilities || ['gpu', 'utility', 'compute'])],
},
};
if (tensorRTLLM.value.environments == null) {
tensorRTLLM.value.environments = [];
}
@@ -131,6 +175,13 @@ const openEdit = (rowData: any): void => {
tensorRTLLM.value.extraHosts = [];
}
drawerVisiable.value = true;
loading.value = true;
try {
await nextTick();
await gpuRef.value?.init(false);
} finally {
loading.value = false;
}
};
const handleClose = () => {
@@ -164,6 +215,7 @@ const rules = reactive({
const formRef = ref<FormInstance>();
const onSubmit = async () => {
if (loading.value || gpuRef.value?.loading) return;
formRef.value?.validate(async (valid) => {
if (!valid) return;
loading.value = true;
@@ -212,5 +264,10 @@ defineExpose({
submit: onSubmit,
loading,
openEdit,
disabled: computed(() => gpuRef.value?.loading),
});
</script>
<style lang="scss" scoped>
@use '@/views/ai/model/components/form-section.scss';
</style>
+119 -101
View File
@@ -16,6 +16,8 @@
/>
<el-form
@validate="(prop, valid) => expandInvalidFormSection(formRef, prop, valid)"
scroll-to-error
v-loading="loading"
@submit.prevent
ref="formRef"
@@ -25,128 +27,137 @@
:rules="formRules"
:validate-on-rule-change="false"
>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="formData.name"></el-input>
</el-form-item>
<el-form-item :label="$t('app.version')" prop="version">
<el-select v-model="formData.version" @change="handleVersionChange">
<el-option
v-for="(version, index) in appVersions"
:key="index"
:label="version"
:value="version"
></el-option>
</el-select>
</el-form-item>
<component :is="sectioned ? 'details' : 'section'" :open="sectioned" :class="{ 'form-section': sectioned }">
<summary v-if="sectioned" class="form-section-header">{{ $t('home.baseInfo') }}</summary>
<div :class="{ 'form-section-body': sectioned }">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="formData.name"></el-input>
</el-form-item>
<el-form-item :label="$t('app.version')" prop="version">
<el-select v-model="formData.version" @change="handleVersionChange">
<el-option
v-for="(version, index) in appVersions"
:key="index"
:label="version"
:value="version"
></el-option>
</el-select>
</el-form-item>
<Params
:key="paramKey"
v-if="showParams"
v-model:form="formData.params"
v-model:params="installParams"
v-model:rules="formRules.params"
:propStart="'params.'"
/>
<Params
:key="paramKey"
v-if="showParams"
v-model:form="formData.params"
v-model:params="installParams"
v-model:rules="formRules.params"
:propStart="'params.'"
/>
</div>
</component>
<slot name="after-params" />
<el-form-item prop="advanced">
<el-checkbox v-model="formData.advanced" :label="$t('app.advanced')" size="large" />
</el-form-item>
<div v-if="formData.advanced">
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input v-model.trim="formData.containerName" :placeholder="$t('app.containerNameHelper')" />
<component :is="sectioned ? 'details' : 'section'" :open="sectioned" :class="{ 'form-section': sectioned }">
<summary v-if="sectioned" class="form-section-header">{{ $t('app.advanced') }}</summary>
<el-form-item v-if="!sectioned" prop="advanced">
<el-checkbox v-model="formData.advanced" :label="$t('app.advanced')" size="large" />
</el-form-item>
<el-form-item prop="allowPort" v-if="!isHostMode">
<el-checkbox v-model="formData.allowPort" :label="$t('app.allowPort')" size="large" />
<span class="input-help">{{ $t('app.allowPortHelper') }}</span>
</el-form-item>
<div v-if="formData.advanced" :class="{ 'form-section-body': sectioned }">
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input v-model.trim="formData.containerName" :placeholder="$t('app.containerNameHelper')" />
</el-form-item>
<el-form-item :label="$t('app.specifyIP')" v-if="formData.allowPort" prop="specifyIP">
<el-input v-model="formData.specifyIP" />
<span class="input-help">{{ $t('app.specifyIPHelper') }}</span>
</el-form-item>
<el-form-item prop="allowPort" v-if="!isHostMode">
<el-checkbox v-model="formData.allowPort" :label="$t('app.allowPort')" size="large" />
<span class="input-help">{{ $t('app.allowPortHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('container.restartPolicy')" prop="restartPolicy">
<el-select v-model="formData.restartPolicy" class="p-w-300">
<el-option :label="$t('container.no')" value="no"></el-option>
<el-option :label="$t('container.always')" value="always"></el-option>
<el-option :label="$t('container.onFailure')" value="on-failure"></el-option>
<el-option :label="$t('container.unlessStopped')" value="unless-stopped"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('app.specifyIP')" v-if="formData.allowPort" prop="specifyIP">
<el-input v-model="formData.specifyIP" />
<span class="input-help">{{ $t('app.specifyIPHelper') }}</span>
</el-form-item>
<el-form-item
:label="$t('container.cpuQuota')"
prop="cpuQuota"
:rules="checkNumberRange(0, limits.cpu)"
>
<el-input type="number" class="!w-2/5" v-model.number="formData.cpuQuota" maxlength="5">
<template #append>{{ $t('app.cpuCore') }}</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.cpu]) }}{{ $t('commons.units.core') }}
</span>
</el-form-item>
<el-form-item :label="$t('container.restartPolicy')" prop="restartPolicy">
<el-select v-model="formData.restartPolicy" class="p-w-300">
<el-option :label="$t('container.no')" value="no"></el-option>
<el-option :label="$t('container.always')" value="always"></el-option>
<el-option :label="$t('container.onFailure')" value="on-failure"></el-option>
<el-option :label="$t('container.unlessStopped')" value="unless-stopped"></el-option>
</el-select>
</el-form-item>
<el-form-item
:label="$t('container.memoryLimit')"
prop="memoryLimit"
:rules="checkNumberRange(0, limits.memory)"
>
<el-input class="!w-2/5" v-model.number="formData.memoryLimit" maxlength="10">
<template #append>
<el-select
v-model="formData.memoryUnit"
placeholder="Select"
class="p-w-100"
@change="changeUnit"
>
<el-option label="MB" value="M" />
<el-option label="GB" value="G" />
</el-select>
</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.memory]) }}{{ formData.memoryUnit }}B
</span>
</el-form-item>
<el-form-item
:label="$t('container.cpuQuota')"
prop="cpuQuota"
:rules="checkNumberRange(0, limits.cpu)"
>
<el-input type="number" class="!w-2/5" v-model.number="formData.cpuQuota" maxlength="5">
<template #append>{{ $t('app.cpuCore') }}</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.cpu]) }}{{ $t('commons.units.core') }}
</span>
</el-form-item>
<el-form-item pro="gpuConfig" v-if="gpuSupport">
<el-checkbox v-model="formData.gpuConfig" :label="$t('app.gpuConfig')" size="large" />
<span class="input-help">{{ $t('app.gpuConfigHelper') }}</span>
</el-form-item>
<el-form-item
:label="$t('container.memoryLimit')"
prop="memoryLimit"
:rules="checkNumberRange(0, limits.memory)"
>
<el-input class="!w-2/5" v-model.number="formData.memoryLimit" maxlength="10">
<template #append>
<el-select
v-model="formData.memoryUnit"
placeholder="Select"
class="p-w-100"
@change="changeUnit"
>
<el-option label="MB" value="M" />
<el-option label="GB" value="G" />
</el-select>
</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.memory]) }}{{ formData.memoryUnit }}B
</span>
</el-form-item>
<el-form-item pro="pullImage">
<el-checkbox v-model="formData.pullImage" :label="$t('app.pullImage')" size="large" />
<span class="input-help">{{ $t('app.pullImageHelper') }}</span>
</el-form-item>
<el-form-item pro="gpuConfig" v-if="gpuSupport && !hideGpuConfig">
<el-checkbox v-model="formData.gpuConfig" :label="$t('app.gpuConfig')" size="large" />
<span class="input-help">{{ $t('app.gpuConfigHelper') }}</span>
</el-form-item>
<PushToNode
v-if="isMaster && isXpackOrEE && batchInstallSupport"
:push-node="formData.pushNode"
:nodes="formData.nodes"
type="app"
@update:push-node="formData.pushNode = $event"
@update:nodes="formData.nodes = $event"
/>
<el-form-item pro="pullImage">
<el-checkbox v-model="formData.pullImage" :label="$t('app.pullImage')" size="large" />
<span class="input-help">{{ $t('app.pullImageHelper') }}</span>
</el-form-item>
<el-form-item prop="editCompose">
<el-checkbox v-model="formData.editCompose" :label="$t('app.editCompose')" size="large" />
<span class="input-help">{{ $t('app.editComposeHelper') }}</span>
</el-form-item>
<PushToNode
v-if="isMaster && isXpackOrEE && batchInstallSupport"
:push-node="formData.pushNode"
:nodes="formData.nodes"
type="app"
@update:push-node="formData.pushNode = $event"
@update:nodes="formData.nodes = $event"
/>
<div v-if="formData.editCompose">
<CodemirrorPro v-model="formData.dockerCompose" mode="yaml" />
<el-form-item prop="editCompose">
<el-checkbox v-model="formData.editCompose" :label="$t('app.editCompose')" size="large" />
<span class="input-help">{{ $t('app.editComposeHelper') }}</span>
</el-form-item>
<div v-if="formData.editCompose">
<CodemirrorPro v-model="formData.dockerCompose" mode="yaml" />
</div>
</div>
</div>
</component>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { expandInvalidFormSection } from '@/views/ai/model/components/form-section';
import { App } from '@/api/interface/app';
import { getAppByKey, getAppDetail, getAppInstalledByID } from '@/api/modules/app';
import { getAppStoreConfig } from '@/api/modules/setting';
@@ -183,6 +194,8 @@ interface Props {
loading?: boolean;
modelValue?: any;
batchInstallSupport?: boolean;
sectioned?: boolean;
hideGpuConfig?: boolean;
}
const limits = ref<Container.ResourceLimit>({
@@ -193,6 +206,7 @@ const limits = ref<Container.ResourceLimit>({
const props = withDefaults(defineProps<Props>(), {
loading: false,
batchInstallSupport: false,
sectioned: false,
});
interface Emits {
@@ -460,3 +474,7 @@ defineExpose({
initClusterForm,
});
</script>
<style lang="scss" scoped>
@use '@/views/ai/model/components/form-section.scss';
</style>
@@ -1,5 +1,10 @@
<template>
<DrawerPro v-model="open" :header="$t('app.param')" @close="handleClose" size="normal">
<DrawerPro
v-model="open"
:header="$t('app.param')"
@close="handleClose"
:size="isMobile ? 'full' : appKey === 'ollama' ? 'large' : 'normal'"
>
<template #buttons>
<el-button type="primary" plain @click="editParam" :disabled="loading" :icon="edit ? '' : 'Edit'">
{{ edit ? $t('app.detail') : $t('commons.button.edit') }}
@@ -60,127 +65,168 @@
</div>
<div v-else v-loading="loading">
<el-alert :title="$t('app.updateHelper')" type="warning" :closable="false" class="common-prompt" />
<el-form @submit.prevent ref="paramForm" :model="paramModel" label-position="top" :rules="rules">
<div v-for="(p, index) in params" :key="index">
<el-form-item :prop="'params.' + p.key" :label="getLabel(p)">
<el-input
v-if="p.type == 'number'"
type="number"
v-model.number="paramModel.params[p.key]"
:disabled="!p.edit"
></el-input>
<el-input
v-else-if="p.type == 'password'"
:type="p.showPassword ? 'text' : 'password'"
v-model="paramModel.params[p.key]"
:disabled="!p.edit"
>
<template #append>
<el-button
:icon="p.showPassword ? 'Hide' : 'View'"
@click="p.showPassword = !p.showPassword"
/>
</template>
</el-input>
<el-select
v-model="paramModel.params[p.key]"
v-else-if="p.type == 'select'"
:multiple="p.multiple"
>
<el-option
v-for="value in p.values"
:key="value.label"
:value="value.value"
:label="value.label"
:disabled="!p.edit"
></el-option>
</el-select>
<el-input v-else v-model.trim="paramModel.params[p.key]" :disabled="!p.edit"></el-input>
</el-form-item>
</div>
<AccountAddress
v-if="appKey === 'ollama'"
ref="accountRef"
show-sync
:form="paramModel"
:host-port="Number(paramModel.params.PANEL_APP_PORT_HTTP) || paramModel.httpPort"
container-port="11434"
:host-mode="paramModel.isHostMode"
/>
<el-form-item prop="advanced">
<el-checkbox v-model="paramModel.advanced" :label="$t('app.advanced')" size="large" />
</el-form-item>
<div v-if="paramModel.advanced">
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input
v-model.trim="paramModel.containerName"
:placeholder="$t('app.containerNameHelper')"
></el-input>
</el-form-item>
<el-form-item prop="allowPort" v-if="!paramModel.isHostMode">
<el-checkbox
v-model="paramModel.allowPort"
:label="$t('app.allowPort')"
size="large"
@change="changeAllowPort"
/>
<span class="input-help">{{ $t('app.allowPortHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('app.specifyIP')" v-if="paramModel.allowPort" prop="specifyIP">
<el-input v-model="paramModel.specifyIP"></el-input>
<span class="input-help">{{ $t('app.specifyIPHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('container.restartPolicy')" prop="restartPolicy">
<el-select v-model="paramModel.restartPolicy" class="p-w-300">
<el-option :label="$t('container.no')" value="no"></el-option>
<el-option :label="$t('container.always')" value="always"></el-option>
<el-option :label="$t('container.onFailure')" value="on-failure"></el-option>
<el-option :label="$t('container.unlessStopped')" value="unless-stopped"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('container.cpuQuota')" prop="cpuQuota">
<el-input type="number" class="!w-2/5" v-model.number="paramModel.cpuQuota" maxlength="5">
<template #append>{{ $t('app.cpuCore') }}</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.cpu]) }}{{ $t('commons.units.core') }}
</span>
</el-form-item>
<el-form-item :label="$t('container.memoryLimit')" prop="memoryLimit">
<el-input class="!w-2/5" v-model.number="paramModel.memoryLimit" maxlength="10">
<template #append>
<el-select
v-model="paramModel.memoryUnit"
placeholder="Select"
class="!w-[85px]"
@change="changeUnit"
<el-form
@validate="(prop, valid) => expandInvalidFormSection(paramForm, prop, valid)"
scroll-to-error
@submit.prevent
ref="paramForm"
:model="paramModel"
label-position="top"
:rules="rules"
>
<component
:is="appKey === 'ollama' ? 'details' : 'section'"
:open="appKey === 'ollama'"
:class="{ 'form-section': appKey === 'ollama' }"
>
<summary v-if="appKey === 'ollama'" class="form-section-header">{{ $t('home.baseInfo') }}</summary>
<div :class="{ 'form-section-body': appKey === 'ollama' }">
<div v-for="(p, index) in params" :key="index">
<el-form-item :prop="'params.' + p.key" :label="getLabel(p)">
<el-input
v-if="p.type == 'number'"
type="number"
v-model.number="paramModel.params[p.key]"
:disabled="!p.edit"
></el-input>
<el-input
v-else-if="p.type == 'password'"
:type="p.showPassword ? 'text' : 'password'"
v-model="paramModel.params[p.key]"
:disabled="!p.edit"
>
<el-option label="MB" value="M" />
<el-option label="GB" value="G" />
<template #append>
<el-button
:icon="p.showPassword ? 'Hide' : 'View'"
@click="p.showPassword = !p.showPassword"
/>
</template>
</el-input>
<el-select
v-model="paramModel.params[p.key]"
v-else-if="p.type == 'select'"
:multiple="p.multiple"
>
<el-option
v-for="value in p.values"
:key="value.label"
:value="value.value"
:label="value.label"
:disabled="!p.edit"
></el-option>
</el-select>
</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.memory]) }}{{ paramModel.memoryUnit }}B
</span>
</el-form-item>
<el-form-item prop="editCompose">
<el-checkbox v-model="paramModel.editCompose" :label="$t('app.editCompose')" size="large" />
<span class="input-help">{{ $t('app.editComposeHelper') }}</span>
</el-form-item>
<div v-if="paramModel.editCompose">
<el-button @click="openDiff()" type="primary" class="!mb-2">
{{ $t('app.showDiff') }}
</el-button>
<CodemirrorPro v-model="paramModel.dockerCompose" mode="yaml"></CodemirrorPro>
<el-input v-else v-model.trim="paramModel.params[p.key]" :disabled="!p.edit"></el-input>
</el-form-item>
</div>
</div>
</div>
</component>
<GPUConfig v-if="showModelGPU" ref="gpuRef" v-model="paramModel.gpu" />
<details v-if="appKey === 'ollama'" open class="form-section">
<summary class="form-section-header">
<span>{{ $t('aiTools.model.syncModelAccount') }}</span>
<el-switch
@click.stop
@keydown.stop
v-model="paramModel.syncModelAccount"
:aria-label="$t('aiTools.model.syncModelAccount')"
/>
</summary>
<div v-show="paramModel.syncModelAccount" class="form-section-body">
<AccountAddress
ref="accountRef"
show-sync
hide-sync-toggle
:form="paramModel"
:host-port="Number(paramModel.params.PANEL_APP_PORT_HTTP) || paramModel.httpPort"
container-port="11434"
:host-mode="paramModel.isHostMode"
/>
</div>
</details>
<component
:is="appKey === 'ollama' ? 'details' : 'section'"
:open="appKey === 'ollama'"
:class="{ 'form-section': appKey === 'ollama' }"
>
<summary v-if="appKey === 'ollama'" class="form-section-header">{{ $t('app.advanced') }}</summary>
<el-form-item v-if="appKey !== 'ollama'" prop="advanced">
<el-checkbox v-model="paramModel.advanced" :label="$t('app.advanced')" size="large" />
</el-form-item>
<div
v-if="appKey === 'ollama' || paramModel.advanced"
:class="{ 'form-section-body': appKey === 'ollama' }"
>
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input
v-model.trim="paramModel.containerName"
:placeholder="$t('app.containerNameHelper')"
></el-input>
</el-form-item>
<el-form-item prop="allowPort" v-if="!paramModel.isHostMode">
<el-checkbox
v-model="paramModel.allowPort"
:label="$t('app.allowPort')"
size="large"
@change="changeAllowPort"
/>
<span class="input-help">{{ $t('app.allowPortHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('app.specifyIP')" v-if="paramModel.allowPort" prop="specifyIP">
<el-input v-model="paramModel.specifyIP"></el-input>
<span class="input-help">{{ $t('app.specifyIPHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('container.restartPolicy')" prop="restartPolicy">
<el-select v-model="paramModel.restartPolicy" class="p-w-300">
<el-option :label="$t('container.no')" value="no"></el-option>
<el-option :label="$t('container.always')" value="always"></el-option>
<el-option :label="$t('container.onFailure')" value="on-failure"></el-option>
<el-option :label="$t('container.unlessStopped')" value="unless-stopped"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('container.cpuQuota')" prop="cpuQuota">
<el-input type="number" class="!w-2/5" v-model.number="paramModel.cpuQuota" maxlength="5">
<template #append>{{ $t('app.cpuCore') }}</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.cpu]) }}{{ $t('commons.units.core') }}
</span>
</el-form-item>
<el-form-item :label="$t('container.memoryLimit')" prop="memoryLimit">
<el-input class="!w-2/5" v-model.number="paramModel.memoryLimit" maxlength="10">
<template #append>
<el-select
v-model="paramModel.memoryUnit"
placeholder="Select"
class="!w-[85px]"
@change="changeUnit"
>
<el-option label="MB" value="M" />
<el-option label="GB" value="G" />
</el-select>
</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.memory]) }}{{ paramModel.memoryUnit }}B
</span>
</el-form-item>
<el-form-item prop="editCompose">
<el-checkbox v-model="paramModel.editCompose" :label="$t('app.editCompose')" size="large" />
<span class="input-help">{{ $t('app.editComposeHelper') }}</span>
</el-form-item>
<div v-if="paramModel.editCompose">
<el-button @click="openDiff()" type="primary" class="!mb-2">
{{ $t('app.showDiff') }}
</el-button>
<CodemirrorPro v-model="paramModel.dockerCompose" mode="yaml"></CodemirrorPro>
</div>
</div>
</component>
</el-form>
</div>
<template #footer v-if="edit">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" :disabled="loading" @click="submit(paramForm)">
<el-button type="primary" :disabled="loading || gpuRef?.loading" @click="submit(paramForm)">
{{ $t('commons.button.confirm') }}
</el-button>
</template>
@@ -188,12 +234,14 @@
</DrawerPro>
</template>
<script lang="ts" setup>
import { expandInvalidFormSection } from '@/views/ai/model/components/form-section';
import AccountAddress from '@/views/ai/model/components/account-address/index.vue';
import GPUConfig from '@/views/ai/model/components/gpu-config/index.vue';
import Diff from '@/views/app-store/installed/upgrade/diff/index.vue';
import { App } from '@/api/interface/app';
import { getAppInstallParams, updateAppInstallParams, updateInstallConfig } from '@/api/modules/app';
import { onBeforeUnmount, reactive, ref } from 'vue';
import { computed, nextTick, onBeforeUnmount, reactive, ref } from 'vue';
import { FormInstance } from 'element-plus';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { MsgError, MsgSuccess } from '@/utils/message';
@@ -218,8 +266,9 @@ interface EditForm extends App.InstallParams {
default: any;
}
const props = defineProps({ modelGPU: Boolean });
const emit = defineEmits(['close']);
const { currentNode } = useGlobalStore();
const { currentNode, isMobile } = useGlobalStore();
const open = ref(false);
const loading = ref(false);
const params = ref<EditForm[]>();
@@ -228,6 +277,16 @@ const paramForm = ref<FormInstance>();
const paramModel = reactive<any>({
params: {},
});
const advancedFields = [
'memoryLimit',
'cpuQuota',
'memoryUnit',
'allowPort',
'specifyIP',
'containerName',
'restartPolicy',
];
let initialAdvancedConfig: Record<string, unknown> = {};
const rules = reactive({
params: {},
cpuQuota: [Rules.requiredInput, checkNumberRange(0, 999)],
@@ -237,7 +296,9 @@ const rules = reactive({
});
const appType = ref('');
const appKey = ref('');
const showModelGPU = computed(() => props.modelGPU && appKey.value === 'ollama');
const accountRef = ref<InstanceType<typeof AccountAddress>>();
const gpuRef = ref<InstanceType<typeof GPUConfig>>();
const appConfigUpdate = ref<App.AppConfigUpdate>({
installID: 0,
webUI: '',
@@ -305,19 +366,24 @@ const acceptParams = async (props: ParamProps) => {
if (!(await get())) return;
open.value = true;
openConfig.value = false;
if (props.edit) editParam();
if (props.edit) await editParam();
};
const handleClose = () => {
emit('close');
open.value = false;
};
const editParam = () => {
const editParam = async () => {
params.value.forEach((param: EditForm) => {
paramModel.params[param.key] = param.value;
});
edit.value = !edit.value;
loadLimit();
if (edit.value && showModelGPU.value) {
await nextTick();
await gpuRef.value?.init(false);
}
};
const changeAllowPort = () => {
@@ -386,11 +452,21 @@ const get = async () => {
}
appType.value = res.data.type;
appKey.value = res.data.appKey;
if (showModelGPU.value) {
paramModel.gpu = res.data.gpu || {
enabled: false,
mode: 'devices',
deviceIDs: [],
capabilities: ['gpu', 'utility', 'compute'],
error: i18n.global.t('aiTools.model.loadFailed'),
};
}
paramModel.name = res.data.name;
paramModel.httpPort = res.data.httpPort;
paramModel.syncModelAccount = res.data.syncModelAccount;
paramModel.modelAccountBaseURLType = res.data.modelAccountBaseURLType || 'container';
paramModel.modelAccountBaseURL = res.data.modelAccountBaseURL;
initialAdvancedConfig = Object.fromEntries(advancedFields.map((key) => [key, paramModel[key]]));
return true;
} catch (error) {
return false;
@@ -400,13 +476,11 @@ const get = async () => {
};
const submit = async (formEl: FormInstance) => {
if (!formEl) return;
if (!formEl || loading.value || gpuRef.value?.loading) return;
const node = currentNode.value;
const installId = paramData.value.id;
await formEl.validate((valid) => {
if (!valid) {
return;
}
if (!valid) return;
ElMessageBox.confirm(i18n.global.t('app.updateWarn'), i18n.global.t('commons.button.update'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
@@ -414,24 +488,26 @@ const submit = async (formEl: FormInstance) => {
}).then(async () => {
if (!open.value || node !== currentNode.value || installId !== paramData.value.id) return;
const submitModel: Record<string, any> = { installId, params: paramModel.params };
if (showModelGPU.value) submitModel.gpu = paramModel.gpu;
if (appKey.value === 'ollama') {
submitModel.syncModelAccount = paramModel.syncModelAccount;
submitModel.modelAccountBaseURLType = paramModel.modelAccountBaseURLType;
submitModel.modelAccountBaseURL = accountRef.value.baseURL;
}
if (paramModel.advanced) {
submitModel.advanced = paramModel.advanced;
submitModel.memoryLimit = paramModel.memoryLimit;
submitModel.cpuQuota = paramModel.cpuQuota;
submitModel.memoryUnit = paramModel.memoryUnit;
submitModel.allowPort = paramModel.allowPort;
submitModel.specifyIP = paramModel.specifyIP;
submitModel.containerName = paramModel.containerName;
const updateAdvanced =
appKey.value === 'ollama'
? paramModel.editCompose ||
advancedFields.some((key) => paramModel[key] !== initialAdvancedConfig[key])
: paramModel.advanced;
if (updateAdvanced) {
submitModel.advanced = true;
for (const key of advancedFields) {
submitModel[key] = paramModel[key];
}
if (paramModel.editCompose) {
submitModel.editCompose = paramModel.editCompose;
submitModel.dockerCompose = paramModel.dockerCompose;
}
submitModel.restartPolicy = paramModel.restartPolicy;
}
try {
loading.value = true;
@@ -505,6 +581,7 @@ onBeforeUnmount(() => {
</script>
<style lang="scss" scoped>
@use '@/views/ai/model/components/form-section.scss';
.change-button {
margin-top: 5px;
}