mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-08-30 17:13:08 +08:00
feat(llm): support hami (#25216)
This commit is contained in:
@@ -17,6 +17,7 @@ type InstantModelListInput struct {
|
||||
ModelTag string `json:"model_tag"`
|
||||
ModelId string `json:"model_id"`
|
||||
LlmType string `json:"llm_type"`
|
||||
Source string `json:"source"`
|
||||
Image string `json:"image"`
|
||||
|
||||
Mounts string `json:"mounts"`
|
||||
|
||||
@@ -8,5 +8,5 @@ const (
|
||||
LLM_SGLANG_HF_ENDPOINT = LLM_VLLM_HF_ENDPOINT
|
||||
LLM_SGLANG_CACHE_DIR = "/root/.cache/huggingface"
|
||||
LLM_SGLANG_BASE_PATH = "/data/models"
|
||||
LLM_SGLANG_MODELS_PATH = "/data/models/huggingface"
|
||||
LLM_SGLANG_MODELS_PATH = LLM_SGLANG_BASE_PATH
|
||||
)
|
||||
|
||||
+12
-7
@@ -78,9 +78,14 @@ func (s PortMappings) IsZero() bool {
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
DevType string `json:"dev_type"`
|
||||
Model string `json:"model"`
|
||||
DevicePath string `json:"device_path"`
|
||||
DevType string `json:"dev_type"`
|
||||
SharingMode string `json:"sharing_mode,omitempty"`
|
||||
Model string `json:"model"`
|
||||
DevicePath string `json:"device_path"`
|
||||
// MemoryMb is optional per-device VRAM (MiB) for HAMI. When > 0 it is used
|
||||
// as MemoryMb/MemoryRequest on pod create; otherwise claim is split evenly.
|
||||
MemoryMb int `json:"memory_mb,omitempty"`
|
||||
SmUtilLimit int `json:"sm_util_limit,omitempty"`
|
||||
}
|
||||
|
||||
type Devices []Device
|
||||
@@ -139,6 +144,10 @@ type LLMSkuDetails struct {
|
||||
// Inference backend version and parameters
|
||||
BackendVersion string `json:"backend_version"`
|
||||
BackendParameters []string `json:"backend_parameters,omitempty"`
|
||||
|
||||
// VramClaimMb is computed from mounted InstantModel weight_size_bytes
|
||||
// (EstimateClaimMb). Not persisted on the SKU row.
|
||||
VramClaimMb int `json:"vram_claim_mb"`
|
||||
}
|
||||
|
||||
type MountedAppResourceDetails struct {
|
||||
@@ -151,10 +160,6 @@ type LLMSKuBaseCreateInput struct {
|
||||
Cpu int `json:"cpu"`
|
||||
Memory int `json:"memory"`
|
||||
Bandwidth int `json:"bandwidth"`
|
||||
// VramClaimMb is the estimated VRAM (MiB) the inference instance will
|
||||
// require. Optional — if 0, the deployment create task will auto-fill it
|
||||
// from the mounted InstantModel's weight_size_bytes.
|
||||
VramClaimMb int `json:"vram_claim_mb,omitempty"`
|
||||
|
||||
Volumes *Volumes `json:"volumes"`
|
||||
HostPaths *HostPaths `json:"host_paths"`
|
||||
|
||||
@@ -8,5 +8,5 @@ const (
|
||||
LLM_VLLM_HF_ENDPOINT = "https://hf-mirror.com"
|
||||
LLM_VLLM_CACHE_DIR = "/root/.cache/huggingface"
|
||||
LLM_VLLM_BASE_PATH = "/data/models"
|
||||
LLM_VLLM_MODELS_PATH = "/data/models/huggingface"
|
||||
LLM_VLLM_MODELS_PATH = LLM_VLLM_BASE_PATH
|
||||
)
|
||||
|
||||
@@ -96,6 +96,7 @@ func (i isolatedDevice) ValidateCreateData(ctx context.Context, userCred mcclien
|
||||
return nil, httperrors.NewInputParameterError("index %d is large than isolated device size %d", index, len(podDevs))
|
||||
}
|
||||
isoDev.Id = podDevs[index].IsolatedDeviceId
|
||||
isoDev.GuestIsolatedDeviceIndex = int(podDevs[index].Index)
|
||||
// remove index
|
||||
isoDev.Index = nil
|
||||
} else {
|
||||
@@ -107,6 +108,7 @@ func (i isolatedDevice) ValidateCreateData(ctx context.Context, userCred mcclien
|
||||
d := podDevs[i].GetIsolatedDevice()
|
||||
if d.GetId() == isoDev.Id || d.GetName() == isoDev.Id {
|
||||
isoDev.Id = d.GetId()
|
||||
isoDev.GuestIsolatedDeviceIndex = int(podDevs[i].Index)
|
||||
foundDisk = true
|
||||
host := d.GetHost()
|
||||
if host.HostType != api.HOST_TYPE_CONTAINER {
|
||||
|
||||
@@ -128,6 +128,7 @@ func probeNvidiaGpus(sharingMode string, manager isolated_device.IContainerDevic
|
||||
}
|
||||
dev := nvidiaGpuUsages[pciAddr].nvidiaGPU
|
||||
dev.manager = manager
|
||||
dev.SetSharingMode(sharingMode)
|
||||
res = append(res, nvidiaGpuUsages[pciAddr].nvidiaGPU)
|
||||
}
|
||||
nvidiaGpuUsages = nil
|
||||
|
||||
@@ -810,6 +810,10 @@ func (dev *SBaseDevice) GetSharingMode() string {
|
||||
return dev.sharingMode
|
||||
}
|
||||
|
||||
func (dev *SBaseDevice) SetSharingMode(mode string) {
|
||||
dev.sharingMode = mode
|
||||
}
|
||||
|
||||
func (dev *SBaseDevice) GetPfName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -88,27 +88,7 @@ func (c *comfyui) GetContainerSpec(ctx context.Context, llm *models.SLLM, image
|
||||
},
|
||||
}
|
||||
|
||||
effDevs := models.GetEffectiveDevices(llm, sku)
|
||||
if len(devices) == 0 && effDevs != nil && len(*effDevs) > 0 {
|
||||
for i := range *effDevs {
|
||||
index := i
|
||||
spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{
|
||||
Type: commonapi.CONTAINER_DEVICE_TYPE_ISOLATED_DEVICE,
|
||||
IsolatedDevice: &computeapi.ContainerIsolatedDevice{
|
||||
Index: &index,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if len(devices) > 0 {
|
||||
for i := range devices {
|
||||
spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{
|
||||
Type: commonapi.CONTAINER_DEVICE_TYPE_ISOLATED_DEVICE,
|
||||
IsolatedDevice: &computeapi.ContainerIsolatedDevice{
|
||||
Id: devices[i].Id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
appendContainerIsolatedDevices(&spec, llm, sku, devices)
|
||||
|
||||
// Volume Mounts, see: https://github.com/YanWenKun/ComfyUI-Docker?tab=readme-ov-file#quick-start---nvidia-gpu
|
||||
diskIndex := 0
|
||||
|
||||
@@ -18,8 +18,12 @@ func TestAppendContainerIsolatedDevicesFromSku(t *testing.T) {
|
||||
if len(spec.Devices) != 1 {
|
||||
t.Fatalf("devices len = %d", len(spec.Devices))
|
||||
}
|
||||
if spec.Devices[0].IsolatedDevice == nil || spec.Devices[0].IsolatedDevice.Index == nil || *spec.Devices[0].IsolatedDevice.Index != 0 {
|
||||
t.Fatalf("device index = %#v", spec.Devices[0].IsolatedDevice)
|
||||
iso := spec.Devices[0].IsolatedDevice
|
||||
if iso == nil || iso.Index == nil || *iso.Index != 0 {
|
||||
t.Fatalf("device index = %#v", iso)
|
||||
}
|
||||
if iso.GuestIsolatedDeviceIndex != 0 {
|
||||
t.Fatalf("guest_isolated_device_index = %d", iso.GuestIsolatedDeviceIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +35,9 @@ func TestAppendContainerIsolatedDevicesById(t *testing.T) {
|
||||
if len(spec.Devices) != 1 || spec.Devices[0].IsolatedDevice == nil || spec.Devices[0].IsolatedDevice.Id != "gpu-1" {
|
||||
t.Fatalf("devices = %#v", spec.Devices)
|
||||
}
|
||||
if spec.Devices[0].IsolatedDevice.GuestIsolatedDeviceIndex != 0 {
|
||||
t.Fatalf("guest_isolated_device_index = %d", spec.Devices[0].IsolatedDevice.GuestIsolatedDeviceIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopUiTitle(t *testing.T) {
|
||||
|
||||
@@ -128,7 +128,8 @@ func appendContainerIsolatedDevices(spec *computeapi.ContainerSpec, llm *models.
|
||||
spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{
|
||||
Type: commonapi.CONTAINER_DEVICE_TYPE_ISOLATED_DEVICE,
|
||||
IsolatedDevice: &computeapi.ContainerIsolatedDevice{
|
||||
Index: &index,
|
||||
Index: &index,
|
||||
GuestIsolatedDeviceIndex: index,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -138,7 +139,8 @@ func appendContainerIsolatedDevices(spec *computeapi.ContainerSpec, llm *models.
|
||||
spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{
|
||||
Type: commonapi.CONTAINER_DEVICE_TYPE_ISOLATED_DEVICE,
|
||||
IsolatedDevice: &computeapi.ContainerIsolatedDevice{
|
||||
Id: devices[i].Id,
|
||||
Id: devices[i].Id,
|
||||
GuestIsolatedDeviceIndex: i,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func downloadHuggingFaceSnapshot(
|
||||
}
|
||||
|
||||
modelBase := filepath.Base(modelName)
|
||||
localDir := filepath.Join(tmpDir, "huggingface", modelBase)
|
||||
localDir := filepath.Join(tmpDir, modelBase)
|
||||
if err := os.MkdirAll(localDir, 0755); err != nil {
|
||||
return "", nil, errors.Wrap(err, "mkdir local model dir")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package llm_container
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
)
|
||||
|
||||
func TestModelsPathIsFlatBase(t *testing.T) {
|
||||
if api.LLM_VLLM_MODELS_PATH != api.LLM_VLLM_BASE_PATH {
|
||||
t.Fatalf("LLM_VLLM_MODELS_PATH = %q, want %q", api.LLM_VLLM_MODELS_PATH, api.LLM_VLLM_BASE_PATH)
|
||||
}
|
||||
if api.LLM_SGLANG_MODELS_PATH != api.LLM_SGLANG_BASE_PATH {
|
||||
t.Fatalf("LLM_SGLANG_MODELS_PATH = %q, want %q", api.LLM_SGLANG_MODELS_PATH, api.LLM_SGLANG_BASE_PATH)
|
||||
}
|
||||
if api.LLM_VLLM_MODELS_PATH != "/data/models" {
|
||||
t.Fatalf("LLM_VLLM_MODELS_PATH = %q, want /data/models", api.LLM_VLLM_MODELS_PATH)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportModelFlatLayout(t *testing.T) {
|
||||
tmpDir := "/tmp/import-work"
|
||||
modelBase := filepath.Base("Qwen/Qwen3-0.6B")
|
||||
localDir := filepath.Join(tmpDir, modelBase)
|
||||
wantLocal := filepath.Join(tmpDir, "Qwen3-0.6B")
|
||||
if localDir != wantLocal {
|
||||
t.Fatalf("localDir = %q, want %q", localDir, wantLocal)
|
||||
}
|
||||
|
||||
mount := path.Join(api.LLM_VLLM_MODELS_PATH, modelBase)
|
||||
wantMount := "/data/models/Qwen3-0.6B"
|
||||
if mount != wantMount {
|
||||
t.Fatalf("mount = %q, want %q", mount, wantMount)
|
||||
}
|
||||
|
||||
// Mirrors GetImageInternalPathMounts: TrimPrefix(BASE) → path_map key.
|
||||
trimmed := strings.TrimPrefix(mount, api.LLM_VLLM_BASE_PATH)
|
||||
if trimmed != "/Qwen3-0.6B" {
|
||||
t.Fatalf("path_map key = %q, want /Qwen3-0.6B", trimmed)
|
||||
}
|
||||
mapped := path.Join(api.LLM_VLLM, trimmed)
|
||||
if mapped != "vllm/Qwen3-0.6B" {
|
||||
t.Fatalf("path_map value = %q, want vllm/Qwen3-0.6B", mapped)
|
||||
}
|
||||
}
|
||||
@@ -9,17 +9,53 @@ import (
|
||||
)
|
||||
|
||||
func TestBuildVLLMEntrypointScriptMountedModelsFlag(t *testing.T) {
|
||||
sleepScript := buildVLLMEntrypointScript(false, 1, nil, nil)
|
||||
sleepScript := buildVLLMEntrypointScript("", 1, nil, nil)
|
||||
if !strings.Contains(sleepScript, "sleep infinity") {
|
||||
t.Fatalf("expected idle script without mounted models, got %q", sleepScript)
|
||||
t.Fatalf("expected idle script without model path, got %q", sleepScript)
|
||||
}
|
||||
serveScript := buildVLLMEntrypointScript(true, 1, nil, &api.LLMSpecVllm{PreferredModel: "Qwen3-8B"})
|
||||
if strings.Contains(sleepScript, "find ") {
|
||||
t.Fatalf("idle script should not find models, got %q", sleepScript)
|
||||
}
|
||||
|
||||
nested := "/data/models/huggingface/Qwen3-8B"
|
||||
serveScript := buildVLLMEntrypointScript(nested, 1, nil, &api.LLMSpecVllm{PreferredModel: "Qwen3-8B"})
|
||||
if strings.Contains(serveScript, "sleep infinity") {
|
||||
t.Fatalf("expected serve script with mounted models, got %q", serveScript)
|
||||
t.Fatalf("expected serve script with model path, got %q", serveScript)
|
||||
}
|
||||
if !strings.Contains(serveScript, api.LLM_VLLM_EXEC_PATH) {
|
||||
t.Fatalf("expected vllm exec in serve script, got %q", serveScript)
|
||||
}
|
||||
if !strings.Contains(serveScript, nested) {
|
||||
t.Fatalf("expected nested model path in serve script, got %q", serveScript)
|
||||
}
|
||||
if strings.Contains(serveScript, "find ") {
|
||||
t.Fatalf("serve script should not find under MODELS_PATH, got %q", serveScript)
|
||||
}
|
||||
if strings.Contains(serveScript, api.LLM_VLLM_MODELS_PATH+"'") || strings.Contains(serveScript, "mkdir -p '"+api.LLM_VLLM_MODELS_PATH) {
|
||||
t.Fatalf("serve script should not select via MODELS_PATH root, got %q", serveScript)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSGLangEntrypointScriptNestedModelPath(t *testing.T) {
|
||||
sleepScript := buildSGLangEntrypointScript("", 1, nil, nil)
|
||||
if !strings.Contains(sleepScript, "sleep infinity") {
|
||||
t.Fatalf("expected idle script without model path, got %q", sleepScript)
|
||||
}
|
||||
|
||||
nested := "/data/models/huggingface/Qwen3-8B"
|
||||
serveScript := buildSGLangEntrypointScript(nested, 1, nil, &api.LLMSpecSGLang{PreferredModel: "Qwen3-8B"})
|
||||
if strings.Contains(serveScript, "sleep infinity") {
|
||||
t.Fatalf("expected serve script with model path, got %q", serveScript)
|
||||
}
|
||||
if !strings.Contains(serveScript, api.LLM_SGLANG_EXEC_PATH) {
|
||||
t.Fatalf("expected sglang exec in serve script, got %q", serveScript)
|
||||
}
|
||||
if !strings.Contains(serveScript, nested) {
|
||||
t.Fatalf("expected nested model path in serve script, got %q", serveScript)
|
||||
}
|
||||
if strings.Contains(serveScript, "find ") {
|
||||
t.Fatalf("serve script should not find under MODELS_PATH, got %q", serveScript)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalPathSkuEnablesServeEntrypoint(t *testing.T) {
|
||||
@@ -38,9 +74,13 @@ func TestLocalPathSkuEnablesServeEntrypoint(t *testing.T) {
|
||||
LocalPath: "/data/models/Qwen3-8B",
|
||||
}
|
||||
sku.HostPaths = &hostPaths
|
||||
postOverlaysLen := 0
|
||||
hasMountedModels := postOverlaysLen > 0 || models.SkuHasLocalHostPathModel(sku)
|
||||
if !hasMountedModels {
|
||||
t.Fatal("expected local_path sku to count as having mounted models")
|
||||
paths := models.CollectContainerModelMountPaths(nil, sku)
|
||||
modelPath := models.PickContainerModelMountPath(paths, "Qwen3-8B")
|
||||
if modelPath != "/data/models/huggingface/Qwen3-8B" {
|
||||
t.Fatalf("expected nested local_path mount, got %q", modelPath)
|
||||
}
|
||||
script := buildVLLMEntrypointScript(modelPath, 1, nil, nil)
|
||||
if !strings.Contains(script, modelPath) {
|
||||
t.Fatalf("expected script to embed mount path, got %q", script)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func downloadModelScopeSnapshot(
|
||||
}
|
||||
|
||||
modelBase := filepath.Base(modelID)
|
||||
localDir := filepath.Join(tmpDir, "modelscope", modelBase)
|
||||
localDir := filepath.Join(tmpDir, modelBase)
|
||||
if err := os.MkdirAll(localDir, 0755); err != nil {
|
||||
return "", nil, errors.Wrap(err, "mkdir local model dir")
|
||||
}
|
||||
|
||||
@@ -82,27 +82,7 @@ func (o *ollama) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *
|
||||
},
|
||||
}
|
||||
|
||||
effDevs := models.GetEffectiveDevices(llm, sku)
|
||||
if len(devices) == 0 && effDevs != nil && len(*effDevs) > 0 {
|
||||
for i := range *effDevs {
|
||||
index := i
|
||||
spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{
|
||||
Type: commonapi.CONTAINER_DEVICE_TYPE_ISOLATED_DEVICE,
|
||||
IsolatedDevice: &computeapi.ContainerIsolatedDevice{
|
||||
Index: &index,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if len(devices) > 0 {
|
||||
for i := range devices {
|
||||
spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{
|
||||
Type: commonapi.CONTAINER_DEVICE_TYPE_ISOLATED_DEVICE,
|
||||
IsolatedDevice: &computeapi.ContainerIsolatedDevice{
|
||||
Id: devices[i].Id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
appendContainerIsolatedDevices(&spec, llm, sku, devices)
|
||||
|
||||
// // process rootfs limit
|
||||
// var diskIndex *int
|
||||
|
||||
@@ -222,8 +222,13 @@ func (s *sglang) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *
|
||||
if sku != nil {
|
||||
backendParameters = sku.BackendParameters
|
||||
}
|
||||
hasMountedModels := len(postOverlays) > 0 || models.SkuHasLocalHostPathModel(sku)
|
||||
startScript := buildSGLangEntrypointScript(hasMountedModels, tensorParallelSize, backendParameters, effSpec)
|
||||
preferred := ""
|
||||
if effSpec != nil {
|
||||
preferred = effSpec.PreferredModel
|
||||
}
|
||||
modelPath := models.PickContainerModelMountPath(models.CollectContainerModelMountPaths(llm, sku), preferred)
|
||||
hasMountedModels := modelPath != "" || len(postOverlays) > 0 || models.SkuHasLocalHostPathModel(sku)
|
||||
startScript := buildSGLangEntrypointScript(modelPath, tensorParallelSize, backendParameters, effSpec)
|
||||
envs := []*commonapi.ContainerKeyValue{
|
||||
{
|
||||
Key: "HUGGING_FACE_HUB_CACHE",
|
||||
@@ -249,27 +254,7 @@ func (s *sglang) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *
|
||||
spec.StartupProbe = newLLMHTTPStartupProbe(api.LLM_SGLANG_DEFAULT_PORT, "/v1/models")
|
||||
}
|
||||
|
||||
effDevs := models.GetEffectiveDevices(llm, sku)
|
||||
if len(devices) == 0 && effDevs != nil && len(*effDevs) > 0 {
|
||||
for i := range *effDevs {
|
||||
index := i
|
||||
spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{
|
||||
Type: commonapi.CONTAINER_DEVICE_TYPE_ISOLATED_DEVICE,
|
||||
IsolatedDevice: &computeapi.ContainerIsolatedDevice{
|
||||
Index: &index,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if len(devices) > 0 {
|
||||
for i := range devices {
|
||||
spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{
|
||||
Type: commonapi.CONTAINER_DEVICE_TYPE_ISOLATED_DEVICE,
|
||||
IsolatedDevice: &computeapi.ContainerIsolatedDevice{
|
||||
Id: devices[i].Id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
appendContainerIsolatedDevices(&spec, llm, sku, devices)
|
||||
|
||||
diskIndex := 0
|
||||
ctrVols := []*commonapi.ContainerVolumeMount{
|
||||
@@ -596,36 +581,14 @@ func buildSGLangServeFlags(modelPath string, tensorParallelSize int, backendPara
|
||||
)
|
||||
}
|
||||
|
||||
func buildSGLangEntrypointScript(hasMountedModels bool, tensorParallelSize int, backendParameters []string, effSpec *api.LLMSpecSGLang) string {
|
||||
modelsPath := shellQuoteSingle(api.LLM_SGLANG_MODELS_PATH)
|
||||
if !hasMountedModels {
|
||||
return fmt.Sprintf("mkdir -p %s && exec sleep infinity", modelsPath)
|
||||
func buildSGLangEntrypointScript(modelPath string, tensorParallelSize int, backendParameters []string, effSpec *api.LLMSpecSGLang) string {
|
||||
modelPath = strings.TrimSpace(modelPath)
|
||||
if modelPath == "" {
|
||||
return "exec sleep infinity"
|
||||
}
|
||||
|
||||
preferredPath := ""
|
||||
if effSpec != nil && strings.TrimSpace(effSpec.PreferredModel) != "" {
|
||||
preferredPath = path.Join(api.LLM_SGLANG_MODELS_PATH, strings.TrimSpace(effSpec.PreferredModel))
|
||||
}
|
||||
serveCmd := strings.Join(buildSGLangServeFlagsWithModelExpr(
|
||||
`"$model"`,
|
||||
`"$(basename "$model")"`,
|
||||
tensorParallelSize,
|
||||
backendParameters,
|
||||
effSpec,
|
||||
), " ")
|
||||
serveCmd := strings.Join(buildSGLangServeFlags(modelPath, tensorParallelSize, backendParameters, effSpec), " ")
|
||||
return strings.Join([]string{
|
||||
"set -e",
|
||||
fmt.Sprintf("mkdir -p %s", modelsPath),
|
||||
fmt.Sprintf("preferred=%s", shellQuoteSingle(preferredPath)),
|
||||
`if [ -n "$preferred" ] && [ -d "$preferred" ]; then`,
|
||||
` model="$preferred"`,
|
||||
"else",
|
||||
fmt.Sprintf(` model="$(find %s -mindepth 1 -maxdepth 1 -type d | sort | head -n 1)"`, modelsPath),
|
||||
"fi",
|
||||
`if [ -z "$model" ]; then`,
|
||||
` echo "no mounted SGLang model found" >&2`,
|
||||
" exit 1",
|
||||
"fi",
|
||||
fmt.Sprintf("exec %s %s", api.LLM_SGLANG_EXEC_PATH, serveCmd),
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
@@ -198,36 +198,14 @@ func buildVLLMServeFlags(modelPath string, tensorParallelSize int, backendParame
|
||||
)
|
||||
}
|
||||
|
||||
func buildVLLMEntrypointScript(hasMountedModels bool, tensorParallelSize int, backendParameters []string, effSpec *api.LLMSpecVllm) string {
|
||||
modelsPath := shellQuoteSingle(api.LLM_VLLM_MODELS_PATH)
|
||||
if !hasMountedModels {
|
||||
return fmt.Sprintf("mkdir -p %s && exec sleep infinity", modelsPath)
|
||||
func buildVLLMEntrypointScript(modelPath string, tensorParallelSize int, backendParameters []string, effSpec *api.LLMSpecVllm) string {
|
||||
modelPath = strings.TrimSpace(modelPath)
|
||||
if modelPath == "" {
|
||||
return "exec sleep infinity"
|
||||
}
|
||||
|
||||
preferredPath := ""
|
||||
if effSpec != nil && strings.TrimSpace(effSpec.PreferredModel) != "" {
|
||||
preferredPath = path.Join(api.LLM_VLLM_MODELS_PATH, strings.TrimSpace(effSpec.PreferredModel))
|
||||
}
|
||||
serveCmd := strings.Join(buildVLLMServeFlagsWithModelExpr(
|
||||
`"$model"`,
|
||||
`"$(basename "$model")"`,
|
||||
tensorParallelSize,
|
||||
backendParameters,
|
||||
effSpec,
|
||||
), " ")
|
||||
serveCmd := strings.Join(buildVLLMServeFlags(modelPath, tensorParallelSize, backendParameters, effSpec), " ")
|
||||
return strings.Join([]string{
|
||||
"set -e",
|
||||
fmt.Sprintf("mkdir -p %s", modelsPath),
|
||||
fmt.Sprintf("preferred=%s", shellQuoteSingle(preferredPath)),
|
||||
`if [ -n "$preferred" ] && [ -d "$preferred" ]; then`,
|
||||
` model="$preferred"`,
|
||||
"else",
|
||||
fmt.Sprintf(` model="$(find %s -mindepth 1 -maxdepth 1 -type d | sort | head -n 1)"`, modelsPath),
|
||||
"fi",
|
||||
`if [ -z "$model" ]; then`,
|
||||
` echo "no mounted vLLM model found" >&2`,
|
||||
" exit 1",
|
||||
"fi",
|
||||
fmt.Sprintf("exec %s %s", api.LLM_VLLM_EXEC_PATH, serveCmd),
|
||||
}, "\n")
|
||||
}
|
||||
@@ -430,8 +408,13 @@ func (v *vllm) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *mo
|
||||
if sku != nil {
|
||||
backendParameters = sku.BackendParameters
|
||||
}
|
||||
hasMountedModels := len(postOverlays) > 0 || models.SkuHasLocalHostPathModel(sku)
|
||||
startScript := buildVLLMEntrypointScript(hasMountedModels, tensorParallelSize, backendParameters, effSpec)
|
||||
preferred := ""
|
||||
if effSpec != nil {
|
||||
preferred = effSpec.PreferredModel
|
||||
}
|
||||
modelPath := models.PickContainerModelMountPath(models.CollectContainerModelMountPaths(llm, sku), preferred)
|
||||
hasMountedModels := modelPath != "" || len(postOverlays) > 0 || models.SkuHasLocalHostPathModel(sku)
|
||||
startScript := buildVLLMEntrypointScript(modelPath, tensorParallelSize, backendParameters, effSpec)
|
||||
envs := []*commonapi.ContainerKeyValue{
|
||||
{
|
||||
Key: "HUGGING_FACE_HUB_CACHE",
|
||||
@@ -468,27 +451,7 @@ func (v *vllm) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *mo
|
||||
}
|
||||
|
||||
// GPU Devices
|
||||
effDevs := models.GetEffectiveDevices(llm, sku)
|
||||
if len(devices) == 0 && effDevs != nil && len(*effDevs) > 0 {
|
||||
for i := range *effDevs {
|
||||
index := i
|
||||
spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{
|
||||
Type: commonapi.CONTAINER_DEVICE_TYPE_ISOLATED_DEVICE,
|
||||
IsolatedDevice: &computeapi.ContainerIsolatedDevice{
|
||||
Index: &index,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if len(devices) > 0 {
|
||||
for i := range devices {
|
||||
spec.Devices = append(spec.Devices, &computeapi.ContainerDevice{
|
||||
Type: commonapi.CONTAINER_DEVICE_TYPE_ISOLATED_DEVICE,
|
||||
IsolatedDevice: &computeapi.ContainerIsolatedDevice{
|
||||
Id: devices[i].Id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
appendContainerIsolatedDevices(&spec, llm, sku, devices)
|
||||
|
||||
// Volume Mounts
|
||||
diskIndex := 0
|
||||
|
||||
@@ -71,6 +71,7 @@ type SInstantModel struct {
|
||||
ModelId string `width:"128" charset:"ascii" list:"user" create:"optional"`
|
||||
ModelName string `width:"128" charset:"ascii" list:"user" create:"required"`
|
||||
ModelTag string `width:"64" charset:"ascii" list:"user" create:"required"`
|
||||
Source string `width:"32" charset:"ascii" nullable:"false" default:"huggingface" list:"user" create:"optional"`
|
||||
|
||||
ImageId string `width:"128" charset:"ascii" list:"user" create:"optional" update:"user"`
|
||||
|
||||
@@ -119,6 +120,9 @@ func (man *SInstantModelManager) ListItemFilter(
|
||||
if len(input.LlmType) > 0 {
|
||||
q = q.Equals("llm_type", input.LlmType)
|
||||
}
|
||||
if len(input.Source) > 0 {
|
||||
q = q.Equals("source", input.Source)
|
||||
}
|
||||
|
||||
if len(input.Image) > 0 {
|
||||
s := auth.GetSession(ctx, userCred, options.Options.Region)
|
||||
@@ -547,18 +551,23 @@ func (man *SInstantModelManager) findInstantModelByImageId(imageId string) (*SIn
|
||||
}
|
||||
|
||||
// FindReadyInstantModel looks up an existing, ready-to-mount InstantModel for
|
||||
// the given (llm_type, model_name, model_tag) triple. Returns (nil, nil) if
|
||||
// none exists. Used by the deployment create flow to dedup catalog imports —
|
||||
// the given (llm_type, model_name, model_tag, source) tuple. Returns (nil, nil)
|
||||
// if none exists. Used by the deployment create flow to dedup catalog imports —
|
||||
// if a previous deployment already brought in Qwen3-8B / vllm / main, we
|
||||
// reuse that row instead of starting another download.
|
||||
//
|
||||
// Only enabled rows match (Enabled=true means import succeeded).
|
||||
func (man *SInstantModelManager) FindReadyInstantModel(llmType, modelName, modelTag string) (*SInstantModel, error) {
|
||||
// Only enabled + active rows match (Enabled=true means import succeeded;
|
||||
// status=active means the glance image is still mountable).
|
||||
func (man *SInstantModelManager) FindReadyInstantModel(llmType, modelName, modelTag, source string) (*SInstantModel, error) {
|
||||
source = defaultInstantModelSource(source)
|
||||
q := man.Query().
|
||||
Equals("llm_type", llmType).
|
||||
Equals("model_name", modelName).
|
||||
Equals("model_tag", modelTag).
|
||||
IsTrue("enabled")
|
||||
Equals("source", source).
|
||||
Equals("status", imageapi.IMAGE_STATUS_ACTIVE).
|
||||
IsTrue("enabled").
|
||||
Desc("created_at")
|
||||
mdls := make([]SInstantModel, 0)
|
||||
if err := db.FetchModelObjects(man, q, &mdls); err != nil {
|
||||
return nil, errors.Wrap(err, "FetchModelObjects")
|
||||
@@ -1091,6 +1100,7 @@ func (man *SInstantModelManager) DoImportWithParent(
|
||||
tempModel.ModelName = input.ModelName
|
||||
tempModel.ModelTag = input.ModelTag
|
||||
tempModel.LlmType = string(input.LlmType)
|
||||
tempModel.Source = defaultInstantModelSource(input.Source)
|
||||
tempModel.ProjectId = userCred.GetProjectId()
|
||||
|
||||
if err := man.TableSpec().Insert(ctx, tempModel); err != nil {
|
||||
@@ -1273,6 +1283,7 @@ func (model *SInstantModel) DoImport(ctx context.Context, userCred mcclient.Toke
|
||||
"model_name": input.ModelName,
|
||||
"model_tag": input.ModelTag,
|
||||
"model_id": modelId,
|
||||
"source": defaultInstantModelSource(input.Source),
|
||||
}
|
||||
|
||||
// upload the image
|
||||
@@ -1301,6 +1312,7 @@ func (model *SInstantModel) DoImport(ctx context.Context, userCred mcclient.Toke
|
||||
model.ModelId = modelId
|
||||
model.ImageId = imageId
|
||||
model.Mounts = mounts
|
||||
model.Source = defaultInstantModelSource(input.Source)
|
||||
if shouldAutoRenameInstantModelImportName(model.Name, model.LlmType, input.ModelName, input.ModelTag) {
|
||||
suffix := extractInstantModelImportNameSuffix(model.Name)
|
||||
model.Name = buildInstantModelFinalName(model.LlmType, modelId, input.ModelTag, suffix)
|
||||
@@ -1451,9 +1463,9 @@ func (model *SInstantModel) GetEstimatedVramSizeMb() int64 {
|
||||
|
||||
// GetDetailsVramRequirement is the per-row endpoint
|
||||
// `GET /instant-models/{id}/vram-requirement`. It returns the heuristic VRAM
|
||||
// requirement computed by the GPUStack-equivalent formula
|
||||
// (weight_size * 1.2 + framework_overhead). When `weight_size_bytes` is 0
|
||||
// (not yet backfilled / unknown source), `vram_required_mb` is also 0.
|
||||
// requirement: weight_size * 1.2 + framework_overhead + KV reserve for the
|
||||
// default max_model_len. When `weight_size_bytes` is 0 (not yet backfilled /
|
||||
// unknown source), `vram_required_mb` is also 0.
|
||||
func (model *SInstantModel) GetDetailsVramRequirement(
|
||||
ctx context.Context,
|
||||
userCred mcclient.TokenCredential,
|
||||
@@ -1462,7 +1474,7 @@ func (model *SInstantModel) GetDetailsVramRequirement(
|
||||
return &apis.InstantModelVramRequirement{
|
||||
LlmType: model.LlmType,
|
||||
WeightSizeBytes: model.WeightSizeBytes,
|
||||
VramRequiredMb: vram.EstimateClaimMb(model.WeightSizeBytes, model.LlmType),
|
||||
VramRequiredMb: vram.EstimateClaimMbWithContext(model.WeightSizeBytes, model.LlmType, apis.LLM_DEFAULT_CONTEXT_TOKENS),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,20 @@ func normalizeInstantModelSource(source string, repoID string) string {
|
||||
return source
|
||||
}
|
||||
|
||||
// defaultInstantModelSource returns a stored InstantModel.source value.
|
||||
// Empty input defaults to huggingface.
|
||||
func defaultInstantModelSource(source string) string {
|
||||
source = strings.ToLower(strings.TrimSpace(source))
|
||||
switch source {
|
||||
case apis.InstantModelSourceModelScope:
|
||||
return apis.InstantModelSourceModelScope
|
||||
case apis.InstantModelSourceHuggingFace, "":
|
||||
return apis.InstantModelSourceHuggingFace
|
||||
default:
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
func resolveImportRepoAndRevision(input apis.InstantModelImportInput) (string, string, string) {
|
||||
repoID := strings.TrimSpace(input.RepoId)
|
||||
source := normalizeInstantModelSource(input.Source, repoID)
|
||||
@@ -66,7 +80,7 @@ func buildInstantModelImportInputFromCreate(input apis.InstantModelCreateInput)
|
||||
|
||||
func normalizeInstantModelCreateInput(input apis.InstantModelCreateInput) apis.InstantModelCreateInput {
|
||||
importInput := buildInstantModelImportInputFromCreate(input)
|
||||
input.Source = importInput.Source
|
||||
input.Source = defaultInstantModelSource(importInput.Source)
|
||||
input.RepoId = importInput.RepoId
|
||||
input.Revision = importInput.Revision
|
||||
input.ModelName = importInput.ModelName
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
apis "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
)
|
||||
|
||||
func TestDefaultInstantModelSource(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"", apis.InstantModelSourceHuggingFace},
|
||||
{"huggingface", apis.InstantModelSourceHuggingFace},
|
||||
{"HuggingFace", apis.InstantModelSourceHuggingFace},
|
||||
{"model_scope", apis.InstantModelSourceModelScope},
|
||||
{"MODEL_SCOPE", apis.InstantModelSourceModelScope},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := defaultInstantModelSource(tc.in); got != tc.want {
|
||||
t.Fatalf("defaultInstantModelSource(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,9 +143,8 @@ func skuFromLLMSkuCreateInput(input *api.LLMSkuCreateInput) *SLLMSku {
|
||||
Source: input.Source,
|
||||
LocalPath: input.LocalPath,
|
||||
SLLMSkuBase: SLLMSkuBase{
|
||||
Devices: input.Devices,
|
||||
VramClaimMb: input.VramClaimMb,
|
||||
HostPaths: input.HostPaths,
|
||||
Devices: input.Devices,
|
||||
HostPaths: input.HostPaths,
|
||||
},
|
||||
}
|
||||
if len(input.PreferHosts) > 0 {
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"fmt"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/pkg/errors"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/apis"
|
||||
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
)
|
||||
|
||||
@@ -26,6 +28,7 @@ func GetLLMBasePodCreateInput(
|
||||
input *api.LLMBaseCreateInput,
|
||||
llmBase *SLLMBase,
|
||||
skuBase *SLLMSkuBase,
|
||||
vramClaimMb int,
|
||||
eip string,
|
||||
) (*computeapi.ServerCreateInput, error) {
|
||||
data := computeapi.ServerCreateInput{}
|
||||
@@ -65,21 +68,42 @@ func GetLLMBasePodCreateInput(
|
||||
effectiveDevices := getEffectiveDevices(llmBase, skuBase)
|
||||
if effectiveDevices != nil && !effectiveDevices.IsZero() {
|
||||
data.IsolatedDevices = make([]*computeapi.IsolatedDeviceConfig, 0)
|
||||
devices := *effectiveDevices
|
||||
// Evenly split the SKU's vram_claim_mb across requested devices and
|
||||
// stamp it onto each request entry. Ceiling division so the sum is
|
||||
// never less than the claim (1 device → claim itself; 2 devices and
|
||||
// 40 GiB claim → 20 GiB each).
|
||||
perDevMinMemMb := 0
|
||||
if skuBase.VramClaimMb > 0 && len(devices) > 0 {
|
||||
perDevMinMemMb = (skuBase.VramClaimMb + len(devices) - 1) / len(devices)
|
||||
devices := make(api.Devices, len(*effectiveDevices))
|
||||
copy(devices, *effectiveDevices)
|
||||
for i := range devices {
|
||||
normalizeLLMSkuDevice(&devices[i])
|
||||
}
|
||||
hasHAMINeedingClaim := false
|
||||
for i := range devices {
|
||||
if devices[i].SharingMode == computeapi.DEVICE_SHARING_MODE_HAMI && devices[i].MemoryMb <= 0 {
|
||||
hasHAMINeedingClaim = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasHAMINeedingClaim && vramClaimMb <= 0 {
|
||||
return nil, errors.Wrap(httperrors.ErrInputParameter,
|
||||
"vram claim is 0 for HAMI devices: set devices[].memory_mb, mount InstantModel with weight_size_bytes, or use a non-HAMI sharing_mode")
|
||||
}
|
||||
// Evenly split estimated vram claim across requested devices when a
|
||||
// device does not set memory_mb. Ceiling division so the sum is never
|
||||
// less than the claim.
|
||||
perDevFromClaim := 0
|
||||
if vramClaimMb > 0 && len(devices) > 0 {
|
||||
perDevFromClaim = (vramClaimMb + len(devices) - 1) / len(devices)
|
||||
}
|
||||
for i := 0; i < len(devices); i++ {
|
||||
memMb := devices[i].MemoryMb
|
||||
if memMb <= 0 {
|
||||
memMb = perDevFromClaim
|
||||
}
|
||||
isolatedDevice := &computeapi.IsolatedDeviceConfig{
|
||||
DevType: devices[i].DevType,
|
||||
Model: devices[i].Model,
|
||||
DevicePath: devices[i].DevicePath,
|
||||
MemoryMb: perDevMinMemMb,
|
||||
DevType: devices[i].DevType,
|
||||
SharingMode: devices[i].SharingMode,
|
||||
Model: devices[i].Model,
|
||||
DevicePath: devices[i].DevicePath,
|
||||
MemoryMb: memMb,
|
||||
MemoryRequest: memMb,
|
||||
SmUtilLimit: devices[i].SmUtilLimit,
|
||||
}
|
||||
data.IsolatedDevices = append(data.IsolatedDevices, isolatedDevice)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/pkg/errors"
|
||||
"yunion.io/x/pkg/utils"
|
||||
|
||||
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/llm/options"
|
||||
@@ -21,7 +24,7 @@ import (
|
||||
const (
|
||||
autoGpuMemoryUtilizationSafetyFactor = 1.10
|
||||
autoGpuMemoryUtilizationMin = 0.05
|
||||
autoGpuMemoryUtilizationMax = 0.95
|
||||
autoGpuMemoryUtilizationMax = 1.0
|
||||
|
||||
sglangAutoGpuMemoryMetadataReserveMB = 512
|
||||
)
|
||||
@@ -107,6 +110,18 @@ func calculateAutoGpuMemoryUtilization(requiredVramMB int64, gpuMemoryMB int64,
|
||||
return calculateAutoGpuMemoryUtilizationFromPerGPURequired(
|
||||
float64(requiredVramMB)/float64(normalizeTensorParallelSize(tensorParallelSize)),
|
||||
gpuMemoryMB,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
func calculateAutoGpuMemoryUtilizationClampMax(requiredVramMB int64, gpuMemoryMB int64, tensorParallelSize int) (float64, error) {
|
||||
if requiredVramMB <= 0 {
|
||||
return 0, errors.Wrap(httperrors.ErrInputParameter, "mounted model gpu_memory_required is empty")
|
||||
}
|
||||
return calculateAutoGpuMemoryUtilizationFromPerGPURequired(
|
||||
float64(requiredVramMB)/float64(normalizeTensorParallelSize(tensorParallelSize)),
|
||||
gpuMemoryMB,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -114,7 +129,7 @@ func instantModelEstimatedVramRequirementMB(model *SInstantModel) int64 {
|
||||
if model == nil {
|
||||
return 0
|
||||
}
|
||||
return int64(vram.EstimateClaimMb(model.WeightSizeBytes, model.LlmType))
|
||||
return int64(vram.EstimateClaimMbWithContext(model.WeightSizeBytes, model.LlmType, api.LLM_DEFAULT_CONTEXT_TOKENS))
|
||||
}
|
||||
|
||||
func runtimeArgKeyIn(key string, keys []string) bool {
|
||||
@@ -275,22 +290,92 @@ func parseTokenLimitValue(value string) (int64, bool) {
|
||||
return int64(math.Ceil(parsed * multiplier)), true
|
||||
}
|
||||
|
||||
func calculateAutoGpuMemoryUtilizationFromPerGPURequired(perGPURequiredMB float64, gpuMemoryMB int64) (float64, error) {
|
||||
func calculateAutoGpuMemoryUtilizationFromPerGPURequired(perGPURequiredMB float64, gpuMemoryMB int64, clampToMax bool) (float64, error) {
|
||||
if gpuMemoryMB <= 0 {
|
||||
return 0, errors.Wrap(httperrors.ErrInputParameter, "gpu memory_mb is empty")
|
||||
}
|
||||
raw := perGPURequiredMB * autoGpuMemoryUtilizationSafetyFactor / float64(gpuMemoryMB)
|
||||
log.Infof("auto gpu_memory_utilization calc: perGPURequiredMB=%.2f gpuMemoryMB=%d safetyFactor=%.2f raw=%.4f clampToMax=%v max=%.2f min=%.2f",
|
||||
perGPURequiredMB, gpuMemoryMB, autoGpuMemoryUtilizationSafetyFactor, raw, clampToMax, autoGpuMemoryUtilizationMax, autoGpuMemoryUtilizationMin)
|
||||
if raw > autoGpuMemoryUtilizationMax {
|
||||
if clampToMax {
|
||||
log.Infof("auto gpu_memory_utilization clamp: raw=%.4f -> %.2f", raw, autoGpuMemoryUtilizationMax)
|
||||
return autoGpuMemoryUtilizationMax, nil
|
||||
}
|
||||
return 0, errors.Wrapf(httperrors.ErrInputParameter,
|
||||
"model requires %.2f GPU memory utilization, exceeds max %.2f", raw, autoGpuMemoryUtilizationMax)
|
||||
}
|
||||
if raw < autoGpuMemoryUtilizationMin {
|
||||
log.Infof("auto gpu_memory_utilization raise: raw=%.4f -> %.2f", raw, autoGpuMemoryUtilizationMin)
|
||||
raw = autoGpuMemoryUtilizationMin
|
||||
}
|
||||
return math.Ceil(raw*100) / 100, nil
|
||||
result := math.Ceil(raw*100) / 100
|
||||
log.Infof("auto gpu_memory_utilization result: %.2f", result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// skuDevicesHaveHami reports whether any SKU device uses HAMI sharing after normalization.
|
||||
func skuDevicesHaveHami(sku *SLLMSku) bool {
|
||||
if sku == nil || sku.Devices == nil {
|
||||
return false
|
||||
}
|
||||
for i := range *sku.Devices {
|
||||
dev := (*sku.Devices)[i]
|
||||
normalizeLLMSkuDevice(&dev)
|
||||
if dev.SharingMode == computeapi.DEVICE_SHARING_MODE_HAMI {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveHamiAllocatedMemoryMB returns the per-GPU HAMI slice size (MiB) used as
|
||||
// the visible GPU memory for auto gpu-memory-utilization. Prefer device.MemoryMb;
|
||||
// otherwise ceil-split EstimateVramClaimMb across devices. Returns the minimum
|
||||
// across devices.
|
||||
func resolveHamiAllocatedMemoryMB(sku *SLLMSku) (int64, error) {
|
||||
claim := 0
|
||||
if sku != nil {
|
||||
claim = sku.EstimateVramClaimMb()
|
||||
}
|
||||
return resolveHamiAllocatedMemoryMBWithClaim(sku, claim)
|
||||
}
|
||||
|
||||
func resolveHamiAllocatedMemoryMBWithClaim(sku *SLLMSku, claimMb int) (int64, error) {
|
||||
if sku == nil || sku.Devices == nil || len(*sku.Devices) == 0 {
|
||||
return 0, errors.Wrap(httperrors.ErrInputParameter, "HAMI allocated memory requires GPU devices")
|
||||
}
|
||||
n := len(*sku.Devices)
|
||||
perDevFromClaim := 0
|
||||
if claimMb > 0 && n > 0 {
|
||||
perDevFromClaim = (claimMb + n - 1) / n
|
||||
}
|
||||
var minMem int64
|
||||
for i := range *sku.Devices {
|
||||
dev := (*sku.Devices)[i]
|
||||
normalizeLLMSkuDevice(&dev)
|
||||
mem := dev.MemoryMb
|
||||
if mem <= 0 {
|
||||
mem = perDevFromClaim
|
||||
}
|
||||
if mem <= 0 {
|
||||
return 0, errors.Wrap(httperrors.ErrInputParameter,
|
||||
"HAMI allocated memory is 0: set devices[].memory_mb or mount InstantModel with weight_size_bytes")
|
||||
}
|
||||
if minMem == 0 || int64(mem) < minMem {
|
||||
minMem = int64(mem)
|
||||
}
|
||||
}
|
||||
return minMem, nil
|
||||
}
|
||||
|
||||
func calculateDeploymentAutoGpuMemoryUtilization(sku *SLLMSku, requiredVramMB int64, gpuMemoryMB int64, tensorParallelSize int) (float64, error) {
|
||||
// HAMI visible memory is the slice (MemoryRequest / CUDA_DEVICE_MEMORY_LIMIT),
|
||||
// not the physical card. Caller must pass allocated slice as gpuMemoryMB.
|
||||
// When required ≈ allocated, safety factor may exceed 1.0 — clamp instead of error.
|
||||
if skuDevicesHaveHami(sku) {
|
||||
return calculateAutoGpuMemoryUtilizationClampMax(requiredVramMB, gpuMemoryMB, tensorParallelSize)
|
||||
}
|
||||
if sku != nil && api.LLMContainerType(sku.LLMType) == api.LLM_CONTAINER_SGLANG {
|
||||
return calculateSGLangAutoGpuMemoryUtilization(sku, requiredVramMB, gpuMemoryMB, tensorParallelSize)
|
||||
}
|
||||
@@ -492,14 +577,23 @@ func BuildDeploymentResolvedGpuMemoryLLMSpec(ctx context.Context, userCred mccli
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gpuMemoryMB, err := minGpuMemoryMB(ctx, userCred, sku.Devices)
|
||||
isHami := skuDevicesHaveHami(sku)
|
||||
var gpuMemoryMB int64
|
||||
if isHami {
|
||||
gpuMemoryMB, err = resolveHamiAllocatedMemoryMB(sku)
|
||||
} else {
|
||||
gpuMemoryMB, err = minGpuMemoryMB(ctx, userCred, sku.Devices)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("auto gpu_memory_utilization resolve: deploy=%s sku=%s/%s llmType=%s hami=%v requiredModelVramMB=%d (no KV) gpuMemoryMB=%d tensorParallelSize=%d",
|
||||
deploy.Id, sku.Id, sku.Name, sku.LLMType, isHami, requiredVramMB, gpuMemoryMB, tensorParallelSize)
|
||||
utilization, err := calculateDeploymentAutoGpuMemoryUtilization(sku, requiredVramMB, gpuMemoryMB, tensorParallelSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("auto gpu_memory_utilization final: deploy=%s sku=%s utilization=%.2f", deploy.Id, sku.Id, utilization)
|
||||
return buildAutoGpuMemoryUtilizationLLMSpec(sku, utilization)
|
||||
}
|
||||
|
||||
@@ -512,21 +606,12 @@ func maxMountedModelVramRequirementMB(sku *SLLMSku) (int64, error) {
|
||||
if len(modelIds) == 0 {
|
||||
return 0, httperrors.NewInputParameterError("auto_gpu_memory_utilization requires mounted models: configure mounted_models on the LLM SKU")
|
||||
}
|
||||
var maxRequiredVramMB int64
|
||||
for _, modelId := range modelIds {
|
||||
obj, err := GetInstantModelManager().FetchById(modelId)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "fetch InstantModel %s", modelId)
|
||||
}
|
||||
requiredVramMB := instantModelEstimatedVramRequirementMB(obj.(*SInstantModel))
|
||||
if requiredVramMB > maxRequiredVramMB {
|
||||
maxRequiredVramMB = requiredVramMB
|
||||
}
|
||||
}
|
||||
if maxRequiredVramMB <= 0 {
|
||||
// Model weights + framework only; KV headroom belongs in HAMI slice (EstimateVramClaimMb), not util numerator.
|
||||
requiredVramMB := int64(sku.EstimateModelVramMb())
|
||||
if requiredVramMB <= 0 {
|
||||
return 0, errors.Wrap(httperrors.ErrInputParameter, "mounted model vram requirement is empty")
|
||||
}
|
||||
return maxRequiredVramMB, nil
|
||||
return requiredVramMB, nil
|
||||
}
|
||||
|
||||
func minGpuMemoryMB(ctx context.Context, userCred mcclient.TokenCredential, devices *api.Devices) (int64, error) {
|
||||
@@ -556,7 +641,7 @@ func fetchMinIsolatedDeviceMemoryMB(ctx context.Context, userCred mcclient.Token
|
||||
}
|
||||
if len(results.Data) == 0 {
|
||||
return 0, errors.Wrapf(httperrors.ErrResourceNotFound,
|
||||
"unused isolated device not found for %s", isolatedDeviceMemoryFilterDesc(device))
|
||||
"matching isolated device not found for %s", isolatedDeviceMemoryFilterDesc(device))
|
||||
}
|
||||
memory, err := minIsolatedDeviceMemoryMB(results.Data)
|
||||
if err != nil {
|
||||
@@ -567,11 +652,18 @@ func fetchMinIsolatedDeviceMemoryMB(ctx context.Context, userCred mcclient.Token
|
||||
|
||||
func buildIsolatedDeviceMemoryParams(device api.Device) *jsonutils.JSONDict {
|
||||
params := jsonutils.NewDict()
|
||||
params.Set("unused", jsonutils.JSONTrue)
|
||||
dev := device
|
||||
normalizeLLMSkuDevice(&dev)
|
||||
// Exclusive GPUs must be unused. Virtual sharing modes (HAMI/UNLIMITED/MPS)
|
||||
// can still have free VRAM/slots while guest joins exist, so skip unused.
|
||||
if !utils.IsInStringArray(dev.SharingMode, computeapi.VIRTUAL_SHARING_MODES) {
|
||||
params.Set("unused", jsonutils.JSONTrue)
|
||||
}
|
||||
params.Set("show_baremetal_isolated_devices", jsonutils.JSONTrue)
|
||||
setStringArrayParam(params, "dev_type", device.DevType)
|
||||
setStringArrayParam(params, "model", device.Model)
|
||||
setStringArrayParam(params, "device_path", device.DevicePath)
|
||||
setStringArrayParam(params, "dev_type", dev.DevType)
|
||||
setStringArrayParam(params, "sharing_mode", dev.SharingMode)
|
||||
setStringArrayParam(params, "model", dev.Model)
|
||||
setStringArrayParam(params, "device_path", dev.DevicePath)
|
||||
return params
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
)
|
||||
|
||||
func TestSkuDevicesHaveHami(t *testing.T) {
|
||||
hamiSku := &SLLMSku{
|
||||
LLMType: string(api.LLM_CONTAINER_VLLM),
|
||||
SLLMSkuBase: SLLMSkuBase{
|
||||
Devices: &api.Devices{{
|
||||
Model: "NVIDIA A100",
|
||||
DevType: computeapi.CONTAINER_DEV_NVIDIA_HAMI,
|
||||
}},
|
||||
},
|
||||
}
|
||||
if !skuDevicesHaveHami(hamiSku) {
|
||||
t.Fatal("expected HAMI device to be detected")
|
||||
}
|
||||
|
||||
exclusiveSku := &SLLMSku{
|
||||
LLMType: string(api.LLM_CONTAINER_VLLM),
|
||||
SLLMSkuBase: SLLMSkuBase{
|
||||
Devices: &api.Devices{{
|
||||
Model: "NVIDIA A100",
|
||||
DevType: computeapi.GPU_TYPE,
|
||||
SharingMode: computeapi.DEVICE_SHARING_MODE_EXCLUSIVE,
|
||||
}},
|
||||
},
|
||||
}
|
||||
if skuDevicesHaveHami(exclusiveSku) {
|
||||
t.Fatal("exclusive device should not be HAMI")
|
||||
}
|
||||
if skuDevicesHaveHami(nil) {
|
||||
t.Fatal("nil sku should not be HAMI")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveHamiAllocatedMemoryMBWithClaim(t *testing.T) {
|
||||
sku := &SLLMSku{
|
||||
LLMType: string(api.LLM_CONTAINER_VLLM),
|
||||
SLLMSkuBase: SLLMSkuBase{
|
||||
Devices: &api.Devices{{
|
||||
Model: "NVIDIA A100",
|
||||
DevType: computeapi.CONTAINER_DEV_NVIDIA_HAMI,
|
||||
}},
|
||||
},
|
||||
}
|
||||
got, err := resolveHamiAllocatedMemoryMBWithClaim(sku, 4096)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != 4096 {
|
||||
t.Fatalf("allocated = %d, want 4096", got)
|
||||
}
|
||||
|
||||
sku.Devices = &api.Devices{{
|
||||
Model: "NVIDIA A100",
|
||||
DevType: computeapi.CONTAINER_DEV_NVIDIA_HAMI,
|
||||
MemoryMb: 16384,
|
||||
}}
|
||||
got, err = resolveHamiAllocatedMemoryMBWithClaim(sku, 4096)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != 16384 {
|
||||
t.Fatalf("manual memory_mb should win, got %d", got)
|
||||
}
|
||||
|
||||
sku.Devices = &api.Devices{
|
||||
{Model: "A100", DevType: computeapi.CONTAINER_DEV_NVIDIA_HAMI, MemoryMb: 16384},
|
||||
{Model: "A100", DevType: computeapi.CONTAINER_DEV_NVIDIA_HAMI},
|
||||
}
|
||||
got, err = resolveHamiAllocatedMemoryMBWithClaim(sku, 4096)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// second device falls back to ceil(4096/2)=2048; min is 2048
|
||||
if got != 2048 {
|
||||
t.Fatalf("min allocated = %d, want 2048", got)
|
||||
}
|
||||
|
||||
sku.Devices = &api.Devices{{Model: "A100", DevType: computeapi.CONTAINER_DEV_NVIDIA_HAMI}}
|
||||
if _, err := resolveHamiAllocatedMemoryMBWithClaim(sku, 0); err == nil {
|
||||
t.Fatal("expected error when claim and memory_mb are both 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateDeploymentAutoGpuMemoryUtilizationHAMI(t *testing.T) {
|
||||
sku := &SLLMSku{
|
||||
LLMType: string(api.LLM_CONTAINER_VLLM),
|
||||
SLLMSkuBase: SLLMSkuBase{
|
||||
Devices: &api.Devices{{
|
||||
Model: "NVIDIA A100",
|
||||
DevType: computeapi.CONTAINER_DEV_NVIDIA_HAMI,
|
||||
}},
|
||||
},
|
||||
}
|
||||
// required ≈ allocated → clamp to 1.0 (not physical 80GiB; no longer 0.95)
|
||||
got, err := calculateDeploymentAutoGpuMemoryUtilization(sku, 4096, 4096, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != autoGpuMemoryUtilizationMax {
|
||||
t.Fatalf("HAMI util when claim≈allocated = %v, want %v", got, autoGpuMemoryUtilizationMax)
|
||||
}
|
||||
|
||||
// larger manual slice → util below max
|
||||
got, err = calculateDeploymentAutoGpuMemoryUtilization(sku, 4096, 16384, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
want := math.Ceil(4096*autoGpuMemoryUtilizationSafetyFactor/16384*100) / 100
|
||||
if got != want {
|
||||
t.Fatalf("HAMI util with larger slice = %v, want %v", got, want)
|
||||
}
|
||||
if got >= autoGpuMemoryUtilizationMax {
|
||||
t.Fatalf("expected util below max, got %v", got)
|
||||
}
|
||||
|
||||
sglangSku := &SLLMSku{
|
||||
LLMType: string(api.LLM_CONTAINER_SGLANG),
|
||||
SLLMSkuBase: SLLMSkuBase{
|
||||
Devices: &api.Devices{{
|
||||
Model: "NVIDIA A100",
|
||||
DevType: computeapi.CONTAINER_DEV_NVIDIA_HAMI,
|
||||
}},
|
||||
},
|
||||
}
|
||||
got, err = calculateDeploymentAutoGpuMemoryUtilization(sglangSku, 4096, 4096, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected sglang error: %v", err)
|
||||
}
|
||||
if got != autoGpuMemoryUtilizationMax {
|
||||
t.Fatalf("HAMI sglang util = %v, want %v", got, autoGpuMemoryUtilizationMax)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateDeploymentAutoGpuMemoryUtilizationExclusive(t *testing.T) {
|
||||
sku := &SLLMSku{
|
||||
LLMType: string(api.LLM_CONTAINER_VLLM),
|
||||
SLLMSkuBase: SLLMSkuBase{
|
||||
Devices: &api.Devices{{
|
||||
Model: "NVIDIA A100",
|
||||
DevType: computeapi.GPU_TYPE,
|
||||
SharingMode: computeapi.DEVICE_SHARING_MODE_EXCLUSIVE,
|
||||
}},
|
||||
},
|
||||
}
|
||||
got, err := calculateDeploymentAutoGpuMemoryUtilization(sku, 4096, 80*1024, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
want, err := calculateAutoGpuMemoryUtilization(4096, 80*1024, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("reference calc error: %v", err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("exclusive util = %v, want %v", got, want)
|
||||
}
|
||||
if got >= autoGpuMemoryUtilizationMax {
|
||||
t.Fatalf("exclusive util on large GPU should be below max, got %v", got)
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,6 @@ func TestSkuCanAutoGpuMemoryUtilizationLocalPath(t *testing.T) {
|
||||
if skuCanAutoGpuMemoryUtilization(localSku) {
|
||||
t.Fatal("local_path SKU should not allow auto GPU util")
|
||||
}
|
||||
localSku.VramClaimMb = 16384
|
||||
if skuCanAutoGpuMemoryUtilization(localSku) {
|
||||
t.Fatal("local_path SKU should not allow auto GPU util even with vram_claim_mb")
|
||||
}
|
||||
if !skuCanAutoGpuMemoryUtilization(nil) {
|
||||
t.Fatal("nil sku should keep legacy default true")
|
||||
}
|
||||
@@ -58,7 +54,6 @@ func TestDisableDeploymentAutoGpuMemoryUtilizationForLocalPath(t *testing.T) {
|
||||
|
||||
func TestMaxMountedModelVramRequirementMBLocalPathRejected(t *testing.T) {
|
||||
sku := localPathTestSku()
|
||||
sku.VramClaimMb = 20480
|
||||
_, err := maxMountedModelVramRequirementMB(sku)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for local_path SKU auto GPU util")
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"yunion.io/x/log"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
)
|
||||
|
||||
// CollectContainerModelMountPaths returns container-absolute model directories from
|
||||
// InstantModel.Mounts (effective llm/sku mounted models) and local_path HostPaths.
|
||||
func CollectContainerModelMountPaths(llm *SLLM, sku *SLLMSku) []string {
|
||||
seen := make(map[string]struct{})
|
||||
var out []string
|
||||
add := func(p string) {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[p]; ok {
|
||||
return
|
||||
}
|
||||
seen[p] = struct{}{}
|
||||
out = append(out, p)
|
||||
}
|
||||
|
||||
ids := GetEffectiveMountedModels(llm, sku)
|
||||
if len(ids) > 0 {
|
||||
instModels := make(map[string]SInstantModel)
|
||||
if err := db.FetchModelObjectsByIds(GetInstantModelManager(), "id", ids, &instModels); err != nil {
|
||||
log.Errorf("CollectContainerModelMountPaths FetchModelObjectsByIds: %v", err)
|
||||
} else {
|
||||
for _, id := range ids {
|
||||
inst, ok := instModels[id]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, m := range inst.Mounts {
|
||||
add(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range collectLocalHostPathMountPaths(sku) {
|
||||
add(p)
|
||||
}
|
||||
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func collectLocalHostPathMountPaths(sku *SLLMSku) []string {
|
||||
if !SkuHasLocalHostPathModel(sku) {
|
||||
return nil
|
||||
}
|
||||
key := fmt.Sprintf("%d", 0)
|
||||
var out []string
|
||||
for _, hp := range *sku.HostPaths {
|
||||
if hp.IsZero() || hp.Containers == nil {
|
||||
continue
|
||||
}
|
||||
rel, ok := hp.Containers[key]
|
||||
if !ok || rel == nil {
|
||||
continue
|
||||
}
|
||||
if p := strings.TrimSpace(rel.MountPath); p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// PickContainerModelMountPath selects a model directory from candidates.
|
||||
// preferred may be an absolute path or a basename (e.g. PreferredModel).
|
||||
func PickContainerModelMountPath(paths []string, preferred string) string {
|
||||
if len(paths) == 0 {
|
||||
return ""
|
||||
}
|
||||
preferred = strings.TrimSpace(preferred)
|
||||
if preferred != "" {
|
||||
for _, p := range paths {
|
||||
if p == preferred {
|
||||
return p
|
||||
}
|
||||
}
|
||||
for _, p := range paths {
|
||||
if path.Base(p) == preferred {
|
||||
return p
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths[0]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
)
|
||||
|
||||
func TestPickContainerModelMountPath(t *testing.T) {
|
||||
paths := []string{
|
||||
"/data/models/Other",
|
||||
"/data/models/huggingface/Qwen3-0.6B",
|
||||
}
|
||||
got := PickContainerModelMountPath(paths, "Qwen3-0.6B")
|
||||
if got != "/data/models/huggingface/Qwen3-0.6B" {
|
||||
t.Fatalf("basename preferred: got %q", got)
|
||||
}
|
||||
got = PickContainerModelMountPath(paths, "/data/models/Other")
|
||||
if got != "/data/models/Other" {
|
||||
t.Fatalf("exact preferred: got %q", got)
|
||||
}
|
||||
got = PickContainerModelMountPath(paths, "")
|
||||
if got != "/data/models/Other" {
|
||||
t.Fatalf("empty preferred first sorted: got %q", got)
|
||||
}
|
||||
if PickContainerModelMountPath(nil, "x") != "" {
|
||||
t.Fatal("empty paths should return empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectContainerModelMountPathsLocalHostPath(t *testing.T) {
|
||||
hostPaths := api.HostPaths{
|
||||
{
|
||||
Type: "directory",
|
||||
Path: "/data/models/Qwen3-8B",
|
||||
Containers: api.ContainerHostPathRelations{
|
||||
"0": {MountPath: "/data/models/huggingface/Qwen3-8B"},
|
||||
},
|
||||
},
|
||||
}
|
||||
sku := &SLLMSku{
|
||||
LLMType: string(api.LLM_CONTAINER_VLLM),
|
||||
Source: api.LLM_MODEL_SOURCE_LOCAL_PATH,
|
||||
LocalPath: "/data/models/Qwen3-8B",
|
||||
}
|
||||
sku.HostPaths = &hostPaths
|
||||
got := CollectContainerModelMountPaths(nil, sku)
|
||||
if len(got) != 1 || got[0] != "/data/models/huggingface/Qwen3-8B" {
|
||||
t.Fatalf("local_path mounts: got %#v", got)
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func GetLLMPodCreateInput(
|
||||
llmImage *SLLMImage,
|
||||
eip string,
|
||||
) (*computeapi.ServerCreateInput, error) {
|
||||
data, err := GetLLMBasePodCreateInput(ctx, userCred, &input.LLMBaseCreateInput, &llm.SLLMBase, &sku.SLLMSkuBase, eip)
|
||||
data, err := GetLLMBasePodCreateInput(ctx, userCred, &input.LLMBaseCreateInput, &llm.SLLMBase, &sku.SLLMSkuBase, sku.EstimateVramClaimMb(), eip)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "GetLLMBasePodCreateInput: ")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/llm/utils/vram"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
||||
imagemodules "yunion.io/x/onecloud/pkg/mcclient/modules/image"
|
||||
@@ -170,6 +171,7 @@ func (manager *SLLMSkuManager) FetchCustomizeColumns(
|
||||
} else {
|
||||
for i, sku := range skus {
|
||||
modelIds := sku.GetMountedModels()
|
||||
res[i].VramClaimMb = EstimateVramClaimMbFromInstantModels(sku.LLMType, modelIds, instModels, effectiveMaxModelLen(&sku))
|
||||
if len(modelIds) > 0 {
|
||||
res[i].MountedModelDetails = make([]api.MountedModelInfo, 0)
|
||||
for _, modelId := range modelIds {
|
||||
@@ -185,6 +187,10 @@ func (manager *SLLMSkuManager) FetchCustomizeColumns(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := range skus {
|
||||
res[i].VramClaimMb = 0
|
||||
}
|
||||
}
|
||||
{
|
||||
images := make(map[string]SLLMImage)
|
||||
@@ -323,6 +329,93 @@ func (sku *SLLMSku) GetMountedModels() []string {
|
||||
return drv.GetMountedModels(sku)
|
||||
}
|
||||
|
||||
// EstimateVramClaimMb returns heuristic VRAM (MiB) from the largest mounted
|
||||
// InstantModel's weight_size_bytes, including KV reserve for effective
|
||||
// max_model_len. 0 means unknown / no mounted weights.
|
||||
func (sku *SLLMSku) EstimateVramClaimMb() int {
|
||||
if sku == nil {
|
||||
return 0
|
||||
}
|
||||
return EstimateVramClaimMbFromMountedModels(sku.LLMType, sku.GetMountedModels(), effectiveMaxModelLen(sku))
|
||||
}
|
||||
|
||||
// EstimateModelVramMb returns weight+framework VRAM (MiB) without KV reserve.
|
||||
// Used as the numerator for auto gpu-memory-utilization; HAMI slice sizing
|
||||
// continues to use EstimateVramClaimMb (WithContext).
|
||||
func (sku *SLLMSku) EstimateModelVramMb() int {
|
||||
if sku == nil {
|
||||
return 0
|
||||
}
|
||||
return EstimateModelVramMbFromMountedModels(sku.LLMType, sku.GetMountedModels())
|
||||
}
|
||||
|
||||
// effectiveMaxModelLen returns SKU-configured max-model-len, or the platform default.
|
||||
func effectiveMaxModelLen(sku *SLLMSku) int {
|
||||
if sku == nil {
|
||||
return api.LLM_DEFAULT_CONTEXT_TOKENS
|
||||
}
|
||||
keys := []string{"max-model-len"}
|
||||
if val, ok := tokenLimitFromBackendParameters(sku.BackendParameters, keys); ok && val > 0 {
|
||||
return int(val)
|
||||
}
|
||||
switch api.LLMContainerType(sku.LLMType) {
|
||||
case api.LLM_CONTAINER_VLLM:
|
||||
if sku.LLMSpec != nil && sku.LLMSpec.Vllm != nil {
|
||||
for _, arg := range sku.LLMSpec.Vllm.CustomizedArgs {
|
||||
if arg == nil || !runtimeArgKeyIn(arg.Key, keys) {
|
||||
continue
|
||||
}
|
||||
if val, ok := parseTokenLimitValue(arg.Value); ok && val > 0 {
|
||||
return int(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
case api.LLM_CONTAINER_SGLANG:
|
||||
if val, ok := sglangRuntimeIntArg(sku, keys); ok && val > 0 {
|
||||
return int(val)
|
||||
}
|
||||
}
|
||||
return api.LLM_DEFAULT_CONTEXT_TOKENS
|
||||
}
|
||||
|
||||
func maxMountedInstantModelWeightBytes(modelIds []string) int64 {
|
||||
var maxWeight int64
|
||||
for _, id := range modelIds {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
continue
|
||||
}
|
||||
obj, err := GetInstantModelManager().FetchById(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if w := obj.(*SInstantModel).WeightSizeBytes; w > maxWeight {
|
||||
maxWeight = w
|
||||
}
|
||||
}
|
||||
return maxWeight
|
||||
}
|
||||
|
||||
// EstimateModelVramMbFromMountedModels estimates model VRAM without KV reserve.
|
||||
func EstimateModelVramMbFromMountedModels(llmType string, modelIds []string) int {
|
||||
return vram.EstimateClaimMb(maxMountedInstantModelWeightBytes(modelIds), llmType)
|
||||
}
|
||||
|
||||
// EstimateVramClaimMbFromMountedModels estimates VRAM from InstantModel ids.
|
||||
func EstimateVramClaimMbFromMountedModels(llmType string, modelIds []string, maxModelLen int) int {
|
||||
return vram.EstimateClaimMbWithContext(maxMountedInstantModelWeightBytes(modelIds), llmType, maxModelLen)
|
||||
}
|
||||
|
||||
// EstimateVramClaimMbFromInstantModels uses already-fetched InstantModel rows.
|
||||
func EstimateVramClaimMbFromInstantModels(llmType string, modelIds []string, models map[string]SInstantModel, maxModelLen int) int {
|
||||
var maxWeight int64
|
||||
for _, id := range modelIds {
|
||||
if m, ok := models[id]; ok && m.WeightSizeBytes > maxWeight {
|
||||
maxWeight = m.WeightSizeBytes
|
||||
}
|
||||
}
|
||||
return vram.EstimateClaimMbWithContext(maxWeight, llmType, maxModelLen)
|
||||
}
|
||||
|
||||
func (sku *SLLMSku) GetLLMContainerDriver() ILLMContainerDriver {
|
||||
return GetLLMContainerDriver(api.LLMContainerType(sku.LLMType))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"yunion.io/x/pkg/errors"
|
||||
|
||||
imageapi "yunion.io/x/onecloud/pkg/apis/image"
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
@@ -122,6 +123,9 @@ func EnableInstantModelForUse(ctx context.Context, userCred mcclient.TokenCreden
|
||||
return errors.Wrapf(err, "fetch InstantModel %s", id)
|
||||
}
|
||||
im := obj.(*SInstantModel)
|
||||
if im.Status != imageapi.IMAGE_STATUS_ACTIVE {
|
||||
return errors.Wrapf(errors.ErrInvalidStatus, "cannot enable InstantModel %s of status %s", id, im.Status)
|
||||
}
|
||||
if im.Enabled.IsTrue() {
|
||||
return nil
|
||||
}
|
||||
|
||||
+53
-18
@@ -33,14 +33,9 @@ type SLLMSkuBaseManager struct {
|
||||
type SLLMSkuBase struct {
|
||||
db.SSharableVirtualResourceBase
|
||||
|
||||
Bandwidth int `nullable:"false" default:"0" create:"optional" list:"user" update:"user"`
|
||||
Cpu int `nullable:"false" default:"1" create:"optional" list:"user" update:"user"`
|
||||
Memory int `nullable:"false" default:"512" create:"optional" list:"user" update:"user"`
|
||||
// VramClaimMb is the heuristic VRAM (MiB) needed to start a single SLLM
|
||||
// instance from this SKU. Auto-filled from the largest mounted InstantModel's
|
||||
// WeightSizeBytes via EstimateVramClaimMb; user can override at create/update
|
||||
// time (any explicit non-zero value bypasses the auto-fill). 0 means unknown.
|
||||
VramClaimMb int `nullable:"false" default:"0" create:"optional" list:"user" update:"user"`
|
||||
Bandwidth int `nullable:"false" default:"0" create:"optional" list:"user" update:"user"`
|
||||
Cpu int `nullable:"false" default:"1" create:"optional" list:"user" update:"user"`
|
||||
Memory int `nullable:"false" default:"512" create:"optional" list:"user" update:"user"`
|
||||
Volumes *api.Volumes `charset:"utf8" length:"medium" nullable:"true" list:"user" update:"user" create:"optional"`
|
||||
HostPaths *api.HostPaths `charset:"utf8" length:"medium" nullable:"true" list:"user" update:"user" create:"optional"`
|
||||
PortMappings *api.PortMappings `charset:"utf8" length:"medium" nullable:"true" list:"user" update:"user" create:"optional"`
|
||||
@@ -80,22 +75,56 @@ func (man *SLLMSkuBaseManager) ValidateCreateData(ctx context.Context, userCred
|
||||
return input, errors.Wrap(httperrors.ErrInputParameter, "volumes cannot be empty")
|
||||
}
|
||||
|
||||
// Default DevType to NVIDIA_GPU when callers omit it (UI's "auto by VRAM"
|
||||
// path posts {} for each device). Without this the scheduler's
|
||||
// (DevType, MemoryMb) aggregation key is empty and the VRAM filter
|
||||
// silently no-ops.
|
||||
if input.Devices != nil {
|
||||
for i := range *input.Devices {
|
||||
if (*input.Devices)[i].DevType == "" {
|
||||
(*input.Devices)[i].DevType = computeapi.GPU_TYPE
|
||||
}
|
||||
}
|
||||
if err := normalizeLLMSkuDevices(input.Devices); err != nil {
|
||||
return input, err
|
||||
}
|
||||
|
||||
input.Status = api.STATUS_READY
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// normalizeLLMSkuDevices maps legacy NVIDIA_* DevTypes onto GPU + SharingMode,
|
||||
// and defaults empty DevType/SharingMode to GPU + HAMI.
|
||||
func normalizeLLMSkuDevices(devices *api.Devices) error {
|
||||
if devices == nil || len(*devices) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := range *devices {
|
||||
normalizeLLMSkuDevice(&(*devices)[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeLLMSkuDevice(dev *api.Device) {
|
||||
switch dev.DevType {
|
||||
case "":
|
||||
dev.DevType = computeapi.GPU_TYPE
|
||||
case computeapi.CONTAINER_DEV_NVIDIA_GPU:
|
||||
dev.DevType = computeapi.GPU_TYPE
|
||||
if dev.SharingMode == "" {
|
||||
dev.SharingMode = computeapi.DEVICE_SHARING_MODE_EXCLUSIVE
|
||||
}
|
||||
case computeapi.CONTAINER_DEV_NVIDIA_MPS:
|
||||
dev.DevType = computeapi.GPU_TYPE
|
||||
if dev.SharingMode == "" {
|
||||
dev.SharingMode = computeapi.DEVICE_SHARING_MODE_MPS
|
||||
}
|
||||
case computeapi.CONTAINER_DEV_NVIDIA_GPU_SHARE:
|
||||
dev.DevType = computeapi.GPU_TYPE
|
||||
if dev.SharingMode == "" {
|
||||
dev.SharingMode = computeapi.DEVICE_SHARING_MODE_UNLIMITED
|
||||
}
|
||||
case computeapi.CONTAINER_DEV_NVIDIA_HAMI:
|
||||
dev.DevType = computeapi.GPU_TYPE
|
||||
if dev.SharingMode == "" {
|
||||
dev.SharingMode = computeapi.DEVICE_SHARING_MODE_HAMI
|
||||
}
|
||||
}
|
||||
if dev.SharingMode == "" {
|
||||
dev.SharingMode = computeapi.DEVICE_SHARING_MODE_HAMI
|
||||
}
|
||||
}
|
||||
|
||||
func (skuBase *SLLMSkuBase) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.LLMSkuBaseUpdateInput) (api.LLMSkuBaseUpdateInput, error) {
|
||||
var err error
|
||||
input.SharableVirtualResourceBaseUpdateInput, err = skuBase.SSharableVirtualResourceBase.ValidateUpdateData(ctx, userCred, query, input.SharableVirtualResourceBaseUpdateInput)
|
||||
@@ -130,5 +159,11 @@ func (skuBase *SLLMSkuBase) ValidateUpdateData(ctx context.Context, userCred mcc
|
||||
}
|
||||
input.Volumes = (*api.Volumes)(&volumes)
|
||||
|
||||
if input.Devices != nil {
|
||||
if err := normalizeLLMSkuDevices(input.Devices); err != nil {
|
||||
return input, err
|
||||
}
|
||||
}
|
||||
|
||||
return input, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
"yunion.io/x/onecloud/pkg/llm/utils/vram"
|
||||
)
|
||||
|
||||
func TestNormalizeLLMSkuDeviceDefaults(t *testing.T) {
|
||||
dev := api.Device{}
|
||||
normalizeLLMSkuDevice(&dev)
|
||||
if dev.DevType != computeapi.GPU_TYPE {
|
||||
t.Fatalf("DevType = %q, want %q", dev.DevType, computeapi.GPU_TYPE)
|
||||
}
|
||||
if dev.SharingMode != computeapi.DEVICE_SHARING_MODE_HAMI {
|
||||
t.Fatalf("SharingMode = %q, want %q", dev.SharingMode, computeapi.DEVICE_SHARING_MODE_HAMI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLLMSkuDeviceLegacyTypes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in api.Device
|
||||
wantDevType string
|
||||
wantSharingMode string
|
||||
}{
|
||||
{
|
||||
name: "NVIDIA_GPU_SHARE",
|
||||
in: api.Device{DevType: computeapi.CONTAINER_DEV_NVIDIA_GPU_SHARE},
|
||||
wantDevType: computeapi.GPU_TYPE,
|
||||
wantSharingMode: computeapi.DEVICE_SHARING_MODE_UNLIMITED,
|
||||
},
|
||||
{
|
||||
name: "NVIDIA_MPS",
|
||||
in: api.Device{DevType: computeapi.CONTAINER_DEV_NVIDIA_MPS},
|
||||
wantDevType: computeapi.GPU_TYPE,
|
||||
wantSharingMode: computeapi.DEVICE_SHARING_MODE_MPS,
|
||||
},
|
||||
{
|
||||
name: "NVIDIA_GPU",
|
||||
in: api.Device{DevType: computeapi.CONTAINER_DEV_NVIDIA_GPU},
|
||||
wantDevType: computeapi.GPU_TYPE,
|
||||
wantSharingMode: computeapi.DEVICE_SHARING_MODE_EXCLUSIVE,
|
||||
},
|
||||
{
|
||||
name: "NVIDIA_HAMI",
|
||||
in: api.Device{DevType: computeapi.CONTAINER_DEV_NVIDIA_HAMI},
|
||||
wantDevType: computeapi.GPU_TYPE,
|
||||
wantSharingMode: computeapi.DEVICE_SHARING_MODE_HAMI,
|
||||
},
|
||||
{
|
||||
name: "explicit sharing_mode preserved",
|
||||
in: api.Device{DevType: computeapi.CONTAINER_DEV_NVIDIA_GPU_SHARE, SharingMode: computeapi.DEVICE_SHARING_MODE_MPS},
|
||||
wantDevType: computeapi.GPU_TYPE,
|
||||
wantSharingMode: computeapi.DEVICE_SHARING_MODE_MPS,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dev := tc.in
|
||||
normalizeLLMSkuDevice(&dev)
|
||||
if dev.DevType != tc.wantDevType {
|
||||
t.Fatalf("DevType = %q, want %q", dev.DevType, tc.wantDevType)
|
||||
}
|
||||
if dev.SharingMode != tc.wantSharingMode {
|
||||
t.Fatalf("SharingMode = %q, want %q", dev.SharingMode, tc.wantSharingMode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLLMSkuDevicesAllowsHAMIWithoutStoredVram(t *testing.T) {
|
||||
devs := api.Devices{{Model: "A100"}}
|
||||
if err := normalizeLLMSkuDevices(&devs); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if devs[0].SharingMode != computeapi.DEVICE_SHARING_MODE_HAMI {
|
||||
t.Fatalf("SharingMode = %q", devs[0].SharingMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveMaxModelLen(t *testing.T) {
|
||||
if got := effectiveMaxModelLen(nil); got != api.LLM_DEFAULT_CONTEXT_TOKENS {
|
||||
t.Fatalf("nil sku → %d", got)
|
||||
}
|
||||
sku := &SLLMSku{LLMType: string(api.LLM_CONTAINER_VLLM)}
|
||||
if got := effectiveMaxModelLen(sku); got != api.LLM_DEFAULT_CONTEXT_TOKENS {
|
||||
t.Fatalf("default → %d", got)
|
||||
}
|
||||
sku.BackendParameters = []string{"--max-model-len=4096"}
|
||||
if got := effectiveMaxModelLen(sku); got != 4096 {
|
||||
t.Fatalf("backend param → %d, want 4096", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateVramClaimMbFromInstantModels(t *testing.T) {
|
||||
models := map[string]SInstantModel{
|
||||
"a": {WeightSizeBytes: 1024 * 1024 * 100}, // 100 MiB weights
|
||||
"b": {WeightSizeBytes: 1024 * 1024 * 1000}, // 1000 MiB weights
|
||||
}
|
||||
maxLen := api.LLM_DEFAULT_CONTEXT_TOKENS
|
||||
got := EstimateVramClaimMbFromInstantModels(string(api.LLM_CONTAINER_VLLM), []string{"a", "b"}, models, maxLen)
|
||||
want := vram.EstimateClaimMbWithContext(1024*1024*1000, string(api.LLM_CONTAINER_VLLM), maxLen)
|
||||
if got != want {
|
||||
t.Fatalf("got %d want %d", got, want)
|
||||
}
|
||||
if EstimateVramClaimMbFromInstantModels(string(api.LLM_CONTAINER_VLLM), nil, models, maxLen) != 0 {
|
||||
t.Fatal("empty model ids should yield 0")
|
||||
}
|
||||
|
||||
base := vram.EstimateClaimMb(1024*1024*1000, string(api.LLM_CONTAINER_VLLM))
|
||||
if want <= base {
|
||||
t.Fatalf("claim WithContext (%d) should exceed model-only EstimateClaimMb (%d)", want, base)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLLMBasePodCreateInputHAMIFields(t *testing.T) {
|
||||
devs := api.Devices{{Model: "A100"}}
|
||||
sku := &SLLMSkuBase{
|
||||
Cpu: 4,
|
||||
Memory: 8192,
|
||||
Volumes: &api.Volumes{{SizeMB: 10240}},
|
||||
Devices: &devs,
|
||||
}
|
||||
llm := &SLLMBase{}
|
||||
input := &api.LLMBaseCreateInput{}
|
||||
input.Name = "llm-hami-test"
|
||||
input.AutoStart = true
|
||||
input.ProjectId = "proj-test"
|
||||
input.Nets = []*computeapi.NetworkConfig{{Network: "net1"}}
|
||||
|
||||
out, err := GetLLMBasePodCreateInput(nil, nil, input, llm, sku, 20480, "")
|
||||
if err != nil {
|
||||
t.Fatalf("GetLLMBasePodCreateInput: %v", err)
|
||||
}
|
||||
if len(out.IsolatedDevices) != 1 {
|
||||
t.Fatalf("IsolatedDevices len = %d", len(out.IsolatedDevices))
|
||||
}
|
||||
dev := out.IsolatedDevices[0]
|
||||
if dev.DevType != computeapi.GPU_TYPE {
|
||||
t.Fatalf("DevType = %q", dev.DevType)
|
||||
}
|
||||
if dev.SharingMode != computeapi.DEVICE_SHARING_MODE_HAMI {
|
||||
t.Fatalf("SharingMode = %q", dev.SharingMode)
|
||||
}
|
||||
if dev.MemoryMb != 20480 || dev.MemoryRequest != 20480 {
|
||||
t.Fatalf("MemoryMb=%d MemoryRequest=%d", dev.MemoryMb, dev.MemoryRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLLMBasePodCreateInputHAMIRequiresClaim(t *testing.T) {
|
||||
devs := api.Devices{{Model: "A100"}}
|
||||
sku := &SLLMSkuBase{
|
||||
Cpu: 4,
|
||||
Memory: 8192,
|
||||
Volumes: &api.Volumes{{SizeMB: 10240}},
|
||||
Devices: &devs,
|
||||
}
|
||||
llm := &SLLMBase{}
|
||||
input := &api.LLMBaseCreateInput{}
|
||||
input.Name = "llm-hami-test"
|
||||
input.AutoStart = true
|
||||
input.ProjectId = "proj-test"
|
||||
input.Nets = []*computeapi.NetworkConfig{{Network: "net1"}}
|
||||
|
||||
if _, err := GetLLMBasePodCreateInput(nil, nil, input, llm, sku, 0, ""); err == nil {
|
||||
t.Fatal("expected error when HAMI with zero vram claim")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLLMBasePodCreateInputHAMIManualMemoryMb(t *testing.T) {
|
||||
devs := api.Devices{{Model: "A100", MemoryMb: 8192}}
|
||||
sku := &SLLMSkuBase{
|
||||
Cpu: 4,
|
||||
Memory: 8192,
|
||||
Volumes: &api.Volumes{{SizeMB: 10240}},
|
||||
Devices: &devs,
|
||||
}
|
||||
llm := &SLLMBase{}
|
||||
input := &api.LLMBaseCreateInput{}
|
||||
input.Name = "llm-hami-manual-mem"
|
||||
input.AutoStart = true
|
||||
input.ProjectId = "proj-test"
|
||||
input.Nets = []*computeapi.NetworkConfig{{Network: "net1"}}
|
||||
|
||||
// Manual memory_mb allows claim=0.
|
||||
out, err := GetLLMBasePodCreateInput(nil, nil, input, llm, sku, 0, "")
|
||||
if err != nil {
|
||||
t.Fatalf("GetLLMBasePodCreateInput: %v", err)
|
||||
}
|
||||
if len(out.IsolatedDevices) != 1 {
|
||||
t.Fatalf("IsolatedDevices len = %d", len(out.IsolatedDevices))
|
||||
}
|
||||
dev := out.IsolatedDevices[0]
|
||||
if dev.MemoryMb != 8192 || dev.MemoryRequest != 8192 {
|
||||
t.Fatalf("MemoryMb=%d MemoryRequest=%d want 8192", dev.MemoryMb, dev.MemoryRequest)
|
||||
}
|
||||
|
||||
// Manual memory_mb wins over claim split.
|
||||
out2, err := GetLLMBasePodCreateInput(nil, nil, input, llm, sku, 40960, "")
|
||||
if err != nil {
|
||||
t.Fatalf("GetLLMBasePodCreateInput with claim: %v", err)
|
||||
}
|
||||
if out2.IsolatedDevices[0].MemoryRequest != 8192 {
|
||||
t.Fatalf("manual memory should win over claim, got %d", out2.IsolatedDevices[0].MemoryRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLLMBasePodCreateInputHAMIMixedMemoryFallback(t *testing.T) {
|
||||
devs := api.Devices{
|
||||
{Model: "A100", MemoryMb: 10240},
|
||||
{Model: "A100"},
|
||||
}
|
||||
sku := &SLLMSkuBase{
|
||||
Cpu: 4,
|
||||
Memory: 8192,
|
||||
Volumes: &api.Volumes{{SizeMB: 10240}},
|
||||
Devices: &devs,
|
||||
}
|
||||
llm := &SLLMBase{}
|
||||
input := &api.LLMBaseCreateInput{}
|
||||
input.Name = "llm-hami-mixed"
|
||||
input.AutoStart = true
|
||||
input.ProjectId = "proj-test"
|
||||
input.Nets = []*computeapi.NetworkConfig{{Network: "net1"}}
|
||||
|
||||
claim := 40960
|
||||
perDevFromClaim := (claim + len(devs) - 1) / len(devs)
|
||||
out, err := GetLLMBasePodCreateInput(nil, nil, input, llm, sku, claim, "")
|
||||
if err != nil {
|
||||
t.Fatalf("GetLLMBasePodCreateInput: %v", err)
|
||||
}
|
||||
if out.IsolatedDevices[0].MemoryRequest != 10240 {
|
||||
t.Fatalf("device0 MemoryRequest = %d want 10240", out.IsolatedDevices[0].MemoryRequest)
|
||||
}
|
||||
if out.IsolatedDevices[1].MemoryRequest != perDevFromClaim {
|
||||
t.Fatalf("device1 MemoryRequest = %d want %d", out.IsolatedDevices[1].MemoryRequest, perDevFromClaim)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMPodIsolatedDeviceConfigFromSKU(t *testing.T) {
|
||||
devs := api.Devices{
|
||||
{Model: "A100"},
|
||||
{DevType: computeapi.CONTAINER_DEV_NVIDIA_GPU_SHARE, Model: "A100"},
|
||||
}
|
||||
vramClaimMb := 40960
|
||||
perDev := (vramClaimMb + len(devs) - 1) / len(devs)
|
||||
|
||||
out := make([]*computeapi.IsolatedDeviceConfig, 0, len(devs))
|
||||
for i := range devs {
|
||||
normalizeLLMSkuDevice(&devs[i])
|
||||
out = append(out, &computeapi.IsolatedDeviceConfig{
|
||||
DevType: devs[i].DevType,
|
||||
SharingMode: devs[i].SharingMode,
|
||||
Model: devs[i].Model,
|
||||
DevicePath: devs[i].DevicePath,
|
||||
MemoryMb: perDev,
|
||||
MemoryRequest: perDev,
|
||||
SmUtilLimit: devs[i].SmUtilLimit,
|
||||
})
|
||||
}
|
||||
if out[0].DevType != computeapi.GPU_TYPE || out[0].SharingMode != computeapi.DEVICE_SHARING_MODE_HAMI {
|
||||
t.Fatalf("device0 = %#v", out[0])
|
||||
}
|
||||
if out[1].DevType != computeapi.GPU_TYPE || out[1].SharingMode != computeapi.DEVICE_SHARING_MODE_UNLIMITED {
|
||||
t.Fatalf("device1 = %#v", out[1])
|
||||
}
|
||||
if out[0].MemoryRequest != perDev || out[1].MemoryRequest != perDev {
|
||||
t.Fatalf("MemoryRequest = %d,%d want %d", out[0].MemoryRequest, out[1].MemoryRequest, perDev)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildIsolatedDeviceMemoryParamsDefaultsHAMI(t *testing.T) {
|
||||
params := buildIsolatedDeviceMemoryParams(api.Device{Model: "A100"})
|
||||
if params.Contains("unused") {
|
||||
t.Fatal("HAMI params should not set unused")
|
||||
}
|
||||
devTypes, err := params.GetArray("dev_type")
|
||||
if err != nil || len(devTypes) != 1 {
|
||||
t.Fatalf("dev_type = %v err=%v", params, err)
|
||||
}
|
||||
if s, _ := devTypes[0].GetString(); s != computeapi.GPU_TYPE {
|
||||
t.Fatalf("dev_type = %q", s)
|
||||
}
|
||||
modes, err := params.GetArray("sharing_mode")
|
||||
if err != nil || len(modes) != 1 {
|
||||
t.Fatalf("sharing_mode = %v err=%v", params, err)
|
||||
}
|
||||
if s, _ := modes[0].GetString(); s != computeapi.DEVICE_SHARING_MODE_HAMI {
|
||||
t.Fatalf("sharing_mode = %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildIsolatedDeviceMemoryParamsExclusiveUsesUnused(t *testing.T) {
|
||||
params := buildIsolatedDeviceMemoryParams(api.Device{
|
||||
DevType: computeapi.GPU_TYPE,
|
||||
SharingMode: computeapi.DEVICE_SHARING_MODE_EXCLUSIVE,
|
||||
Model: "A100",
|
||||
})
|
||||
if !params.Contains("unused") {
|
||||
t.Fatal("exclusive params should set unused")
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,10 @@ func handleLLMRouterAgentRoute(ctx context.Context, w http.ResponseWriter, r *ht
|
||||
httperrors.GeneralServerError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
appsrv.SendStruct(w, out)
|
||||
// Wrap with keyword so mcclient/apigateway PerformAction can parse the response.
|
||||
wrapped := jsonutils.NewDict()
|
||||
wrapped.Set(models.GetLLMRouterAgentManager().Keyword(), jsonutils.Marshal(out))
|
||||
appsrv.SendJSON(w, wrapped)
|
||||
}
|
||||
|
||||
func InitHandlers(app *appsrv.Application, isSlave bool) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
"yunion.io/x/onecloud/pkg/llm/models"
|
||||
"yunion.io/x/onecloud/pkg/llm/utils/vram"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/util/logclient"
|
||||
)
|
||||
@@ -68,11 +67,12 @@ func (task *LLMDeploymentCreateTask) OnInit(ctx context.Context, obj db.IStandal
|
||||
case input.ModelSpec != nil:
|
||||
// Mode C: optionally import InstantModel first, then create SKU.
|
||||
// If an enabled InstantModel with the same (llm_type, model_name,
|
||||
// model_tag) already exists, skip the import and reuse it.
|
||||
// model_tag, source) already exists, skip the import and reuse it.
|
||||
if existing, err := models.GetInstantModelManager().FindReadyInstantModel(
|
||||
string(input.ModelSpec.LlmType),
|
||||
input.ModelSpec.ModelName,
|
||||
input.ModelSpec.ModelTag,
|
||||
input.ModelSpec.Source,
|
||||
); err != nil {
|
||||
log.Warningf("FindReadyInstantModel: %s — proceeding with fresh import", err)
|
||||
} else if existing != nil {
|
||||
@@ -222,26 +222,6 @@ func (task *LLMDeploymentCreateTask) createSkuAndReconcile(ctx context.Context,
|
||||
skuSpec.Name = fmt.Sprintf("%s-sku", model.Name)
|
||||
}
|
||||
|
||||
// Auto-fill VramClaimMb from the largest mounted InstantModel's
|
||||
// weight_size_bytes. User-provided non-zero values are respected.
|
||||
if skuSpec.VramClaimMb == 0 {
|
||||
var maxWeight int64
|
||||
for _, id := range skuSpec.MountedModels {
|
||||
obj, err := models.GetInstantModelManager().FetchById(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if w := obj.(*models.SInstantModel).WeightSizeBytes; w > maxWeight {
|
||||
maxWeight = w
|
||||
}
|
||||
}
|
||||
if maxWeight > 0 {
|
||||
skuSpec.VramClaimMb = vram.EstimateClaimMb(maxWeight, skuSpec.LLMType)
|
||||
log.Infof("LLMDeploymentCreateTask: auto vram_claim_mb=%d for sku=%s (weight=%d bytes, llm_type=%s)",
|
||||
skuSpec.VramClaimMb, skuSpec.Name, maxWeight, skuSpec.LLMType)
|
||||
}
|
||||
}
|
||||
|
||||
skuParams := jsonutils.Marshal(skuSpec).(*jsonutils.JSONDict)
|
||||
// __meta__ from VirtualResourceCreateInput marshals as null when Metadata is nil;
|
||||
// keep params clean to avoid surprises in the framework dispatcher.
|
||||
|
||||
@@ -45,6 +45,7 @@ func (task *LLMSkuCreateTask) OnInit(ctx context.Context, obj db.IStandaloneMode
|
||||
string(importInput.LlmType),
|
||||
importInput.ModelName,
|
||||
importInput.ModelTag,
|
||||
importInput.Source,
|
||||
); err != nil {
|
||||
log.Warningf("LLMSkuCreateTask FindReadyInstantModel: %s; importing a fresh InstantModel", err)
|
||||
} else if existing != nil {
|
||||
|
||||
@@ -20,6 +20,10 @@ import api "yunion.io/x/onecloud/pkg/apis/llm"
|
||||
// 3B → ~8.9 GiB
|
||||
// 7B → ~19.0 GiB
|
||||
// 72B → ~164.5 GiB
|
||||
//
|
||||
// EstimateClaimMbWithContext additionally reserves KV headroom for
|
||||
// max_model_len (≈ 256 KiB/token → len/4 MiB) so HAMI slices can satisfy
|
||||
// vLLM's KV check at the configured context length.
|
||||
const (
|
||||
activationOverheadFactor = 1.2
|
||||
llmFrameworkOverheadMB = 2048 // 2 GiB
|
||||
@@ -47,6 +51,26 @@ func EstimateClaimMb(weightSizeBytes int64, llmType string) int {
|
||||
return int(float64(weightMb)*activationOverheadFactor) + overhead
|
||||
}
|
||||
|
||||
// EstimateKvCacheReserveMb returns a conservative KV-cache headroom in MiB
|
||||
// for the given max_model_len. Uses ~256 KiB/token (len/4 MiB). When
|
||||
// maxModelLen <= 0, defaults to api.LLM_DEFAULT_CONTEXT_TOKENS.
|
||||
func EstimateKvCacheReserveMb(maxModelLen int) int {
|
||||
if maxModelLen <= 0 {
|
||||
maxModelLen = api.LLM_DEFAULT_CONTEXT_TOKENS
|
||||
}
|
||||
return maxModelLen / 4
|
||||
}
|
||||
|
||||
// EstimateClaimMbWithContext is EstimateClaimMb plus KV reserve for LLM
|
||||
// backends. Non-LLM / image types ignore maxModelLen (same as EstimateClaimMb).
|
||||
func EstimateClaimMbWithContext(weightSizeBytes int64, llmType string, maxModelLen int) int {
|
||||
base := EstimateClaimMb(weightSizeBytes, llmType)
|
||||
if base <= 0 || !isLLMType(llmType) {
|
||||
return base
|
||||
}
|
||||
return base + EstimateKvCacheReserveMb(maxModelLen)
|
||||
}
|
||||
|
||||
// isLLMType reports whether the backend serves text-generation LLMs that get
|
||||
// the larger framework overhead (CUDA graphs, runtime buffers, KV scratch).
|
||||
func isLLMType(t string) bool {
|
||||
|
||||
@@ -81,3 +81,34 @@ func TestEstimateClaimMb(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateKvCacheReserveMb(t *testing.T) {
|
||||
if got := EstimateKvCacheReserveMb(8192); got != 2048 {
|
||||
t.Fatalf("8192 → %d, want 2048", got)
|
||||
}
|
||||
if got := EstimateKvCacheReserveMb(0); got != api.LLM_DEFAULT_CONTEXT_TOKENS/4 {
|
||||
t.Fatalf("0 → %d, want default/4=%d", got, api.LLM_DEFAULT_CONTEXT_TOKENS/4)
|
||||
}
|
||||
if got := EstimateKvCacheReserveMb(4096); got != 1024 {
|
||||
t.Fatalf("4096 → %d, want 1024", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateClaimMbWithContext(t *testing.T) {
|
||||
weightBytes := int64(1024 * 1024 * 1000) // 1000 MiB
|
||||
base := EstimateClaimMb(weightBytes, string(api.LLM_CONTAINER_VLLM))
|
||||
got := EstimateClaimMbWithContext(weightBytes, string(api.LLM_CONTAINER_VLLM), 8192)
|
||||
if got != base+2048 {
|
||||
t.Fatalf("with context = %d, want base+2048=%d (base=%d)", got, base+2048, base)
|
||||
}
|
||||
// ComfyUI: no KV reserve
|
||||
imgWeight := int64(2 * 1024 * 1024 * 1024)
|
||||
imgBase := EstimateClaimMb(imgWeight, string(api.LLM_CONTAINER_COMFYUI))
|
||||
imgGot := EstimateClaimMbWithContext(imgWeight, string(api.LLM_CONTAINER_COMFYUI), 8192)
|
||||
if imgGot != imgBase {
|
||||
t.Fatalf("comfyui should ignore context, got %d base %d", imgGot, imgBase)
|
||||
}
|
||||
if EstimateClaimMbWithContext(0, string(api.LLM_CONTAINER_VLLM), 8192) != 0 {
|
||||
t.Fatal("zero weight should stay 0")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ type LLMDeploymentCreateOptions struct {
|
||||
SkuStorageType string `help:"SKU storage type, e.g. local" json:"-"`
|
||||
SkuTemplateId string `help:"SKU storage template id" json:"-"`
|
||||
SkuPortMappings []string `help:"port mapping protocol:port[:prefix][:offset][:envs]; repeatable" json:"-"`
|
||||
SkuDevices []string `help:"device model[:path[:dev_type]]; repeatable" json:"-"`
|
||||
SkuDevices []string `help:"device model[:path[:dev_type[:sharing_mode]]]; repeatable" json:"-"`
|
||||
SkuEnv []string `help:"env key=value; repeatable" json:"-"`
|
||||
SkuProperty []string `help:"property key=value; repeatable" json:"-"`
|
||||
SkuMountedModels []string `help:"already-imported InstantModel ref, format name:tag; repeatable" json:"-"`
|
||||
|
||||
@@ -24,7 +24,7 @@ type LLMSkuBaseCreateOptions struct {
|
||||
// DiskOverlay string `help:"disk overlay, e.g. /opt/steam-data/base:/opt/steam-data/games"`
|
||||
TemplateId string
|
||||
PortMappings []string `help:"port mapping in the format of protocol:port[:prefix][:first_port_offset][:env_key=env_value], e.g. tcp:5555:192.168.0.0/16:5:WOLF_BASE_PORT=20000"`
|
||||
Devices []string `help:"device info in the format of model[:path[:dev_type]], e.g. 'GeForce RTX 4060'"`
|
||||
Devices []string `help:"device info in the format of model[:path[:dev_type[:sharing_mode]]], e.g. 'GeForce RTX 4060'"`
|
||||
HostPaths []string `json:"-" help:"host path mount in format path=<host_path>,type=<directory|file>,container_index=<index>,mount_path=<container_path>[,auto_create=<bool>][,read_only=<bool>][,propagation=<private|rslave|rshared>][,fs_user=<uid>][,fs_group=<gid>][,uid=<uid>][,gid=<gid>][,permissions=<mode>]; repeatable"`
|
||||
|
||||
Env []string `help:"env in format of key=value"`
|
||||
@@ -69,7 +69,7 @@ type LLMSkuBaseUpdateOptions struct {
|
||||
// Dpi *int
|
||||
// Fps *int
|
||||
PortMappings []string `help:"port mapping in the format of protocol:port[:prefix][:first_port_offset], e.g. tcp:5555:192.168.0.0/16,10.10.0.0/16:1000"`
|
||||
Devices []string `help:"device info in the format of model[:path[:dev_type]], e.g. QuadraT2A:/dev/nvme1n1, Device::VASTAITECH_GPU"`
|
||||
Devices []string `help:"device info in the format of model[:path[:dev_type[:sharing_mode]]], e.g. QuadraT2A:/dev/nvme1n1, Device::VASTAITECH_GPU"`
|
||||
HostPaths []string `json:"-" help:"host path mount in format path=<host_path>,type=<directory|file>,container_index=<index>,mount_path=<container_path>[,auto_create=<bool>][,read_only=<bool>][,propagation=<private|rslave|rshared>][,fs_user=<uid>][,fs_group=<gid>][,uid=<uid>][,gid=<gid>][,permissions=<mode>]; repeatable"`
|
||||
Env []string `help:"env in the format of key=value, e.g. AUTHENTICATION_PATH=/bupt-test/"`
|
||||
Property []string `help:"extra properties of key=value, e.g. tango32=true"`
|
||||
@@ -364,16 +364,21 @@ func fetchDevices(devStrs []string, dict *jsonutils.JSONDict) {
|
||||
if len(segs) > 0 {
|
||||
devpath := ""
|
||||
devType := ""
|
||||
sharingMode := ""
|
||||
if len(segs) > 1 {
|
||||
devpath = segs[1]
|
||||
}
|
||||
if len(segs) > 2 {
|
||||
devType = segs[2]
|
||||
}
|
||||
if len(segs) > 3 {
|
||||
sharingMode = segs[3]
|
||||
}
|
||||
devs = append(devs, api.Device{
|
||||
Model: segs[0],
|
||||
DevicePath: devpath,
|
||||
DevType: devType,
|
||||
Model: segs[0],
|
||||
DevicePath: devpath,
|
||||
DevType: devType,
|
||||
SharingMode: sharingMode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user