feat(llm): support local_path scheduling with prefer_hosts and deployment restart (#25090)

This commit is contained in:
Zexi Li
2026-07-01 13:11:19 +08:00
committed by GitHub
parent 31e342881e
commit 3431c10196
30 changed files with 1444 additions and 192 deletions
+2
View File
@@ -15,4 +15,6 @@ func init() {
cmd.Delete(new(options.LLMDeploymentDeleteOptions))
cmd.Perform("register-aiproxy", new(options.LLMDeploymentRegisterAiproxyOptions))
cmd.Perform("unregister-aiproxy", new(options.LLMDeploymentUnregisterAiproxyOptions))
cmd.Perform("restart", new(options.LLMDeploymentRestartOptions))
cmd.Perform("syncstatus", new(options.LLMDeploymentSyncstatusOptions))
}
+2 -2
View File
@@ -27,9 +27,9 @@ climc llm-deployment-register-aiproxy my-qwen
climc llm-deployment-unregister-aiproxy my-qwen
```
`unregister-aiproxy` 会清理 aiproxy 侧资源,并将 `auto_register_aiproxy` 置为 false,同时清空 `aiproxy_bindings`。deployment `status` 恢复为副本健康态(`ready` / `partial` / `deploying` 等)。
`unregister-aiproxy` 会清理 aiproxy 侧资源,并将 `auto_register_aiproxy` 置为 false,同时清空 `aiproxy_bindings``aiproxy_sync_status` 置为 `disabled`。deployment `status` 保持副本健康态(`ready` / `partial` / `deploying` 等),不再被网关同步覆盖
网关同步阶段通过 deployment `status` 表达:`aiproxy_pending``aiproxy_syncing``aiproxy_partial``aiproxy_sync_failed`;全部同步成功后恢复为 `ready``partial`
网关同步进度通过独立字段 `aiproxy_sync_status` 表达:`disabled``pending``syncing``synced``partial``failed`
## 查看对应关系
+22 -6
View File
@@ -85,12 +85,18 @@ const (
LLM_DEPLOYMENT_STATUS_DEPLOYING = "deploying"
// Some replicas running but not all (e.g., one died, scale-up in progress).
LLM_DEPLOYMENT_STATUS_PARTIAL = "partial"
// Replica reconcile or syncstatus in progress.
LLM_DEPLOYMENT_STATUS_SYNCING = "syncing"
)
// Aiproxy gateway sync phases (stored in deployment status, not a separate column).
LLM_DEPLOYMENT_STATUS_AIPROXY_PENDING = "aiproxy_pending"
LLM_DEPLOYMENT_STATUS_AIPROXY_SYNCING = "aiproxy_syncing"
LLM_DEPLOYMENT_STATUS_AIPROXY_PARTIAL = "aiproxy_partial"
LLM_DEPLOYMENT_STATUS_AIPROXY_SYNC_FAILED = "aiproxy_sync_failed"
// AiproxySyncStatus values stored on SLLMDeployment.AiproxySyncStatus.
const (
AIPROXY_SYNC_STATUS_DISABLED = "disabled"
AIPROXY_SYNC_STATUS_PENDING = "pending"
AIPROXY_SYNC_STATUS_SYNCING = "syncing"
AIPROXY_SYNC_STATUS_SYNCED = "synced"
AIPROXY_SYNC_STATUS_PARTIAL = "partial"
AIPROXY_SYNC_STATUS_FAILED = "failed"
)
// LLMDeploymentCreateInput is the input for creating a new LLMDeployment deployment.
@@ -119,6 +125,8 @@ type LLMDeploymentCreateInput struct {
AutoStart bool `json:"auto_start"`
// Prefer specific host
PreferHost string `json:"prefer_host"`
// Prefer specific hosts for local_path scheduling (round-robin per replica).
PreferHosts []string `json:"prefer_hosts,omitempty"`
// Host path mounts for instances.
HostPaths *HostPaths `json:"host_paths,omitempty"`
@@ -174,6 +182,12 @@ type LLMDeploymentUpdateInput struct {
AiproxyModelPrefix *string `json:"aiproxy_model_prefix,omitempty"`
}
type LLMDeploymentRestartInput struct {
}
type LLMDeploymentSyncstatusInput struct {
}
// Model source types
const (
LLM_MODEL_SOURCE_HUGGINGFACE = "huggingface"
@@ -207,7 +221,8 @@ type LLMDeploymentDetails struct {
ModelScopeModelId string `json:"model_scope_model_id"`
ModelScopeFilePath string `json:"model_scope_file_path"`
LocalPath string `json:"local_path"`
Categories string `json:"categories"`
PreferHosts []string `json:"prefer_hosts,omitempty"`
Categories []string `json:"categories,omitempty"`
Backend string `json:"backend"`
BackendVersion string `json:"backend_version"`
Replicas int `json:"replicas"`
@@ -229,6 +244,7 @@ type LLMDeploymentDetails struct {
AutoRegisterAiproxy bool `json:"auto_register_aiproxy"`
AiproxyModelPrefix string `json:"aiproxy_model_prefix"`
AiproxyRoutingId string `json:"aiproxy_routing_id"`
AiproxySyncStatus string `json:"aiproxy_sync_status"`
}
// Per-replica aiproxy binding sync status (AiproxyInstanceBinding.sync_status).
+17 -15
View File
@@ -127,17 +127,18 @@ type LLMSkuDetails struct {
LLMSpec *LLMSpec `json:"llm_spec,omitempty"`
// Model source
Source string `json:"source"`
HuggingfaceRepoId string `json:"huggingface_repo_id"`
HuggingfaceFilename string `json:"huggingface_filename"`
ModelScopeModelId string `json:"model_scope_model_id"`
ModelScopeFilePath string `json:"model_scope_file_path"`
LocalPath string `json:"local_path"`
Source string `json:"source"`
HuggingfaceRepoId string `json:"huggingface_repo_id"`
HuggingfaceFilename string `json:"huggingface_filename"`
ModelScopeModelId string `json:"model_scope_model_id"`
ModelScopeFilePath string `json:"model_scope_file_path"`
LocalPath string `json:"local_path"`
PreferHosts []string `json:"prefer_hosts,omitempty"`
// Model categories
Categories string `json:"categories"`
Categories []string `json:"categories,omitempty"`
// Inference backend version and parameters
BackendVersion string `json:"backend_version"`
BackendParameters string `json:"backend_parameters"`
BackendVersion string `json:"backend_version"`
BackendParameters []string `json:"backend_parameters,omitempty"`
}
type MountedAppResourceDetails struct {
@@ -212,12 +213,13 @@ type LLMSkuCreateInput struct {
LLMSpec *LLMSpec `json:"llm_spec,omitempty"`
// Model source
Source string `json:"source"`
HuggingfaceRepoId string `json:"huggingface_repo_id"`
HuggingfaceFilename string `json:"huggingface_filename"`
ModelScopeModelId string `json:"model_scope_model_id"`
ModelScopeFilePath string `json:"model_scope_file_path"`
LocalPath string `json:"local_path"`
Source string `json:"source"`
HuggingfaceRepoId string `json:"huggingface_repo_id"`
HuggingfaceFilename string `json:"huggingface_filename"`
ModelScopeModelId string `json:"model_scope_model_id"`
ModelScopeFilePath string `json:"model_scope_file_path"`
LocalPath string `json:"local_path"`
PreferHosts []string `json:"prefer_hosts,omitempty"`
// Model categories
Categories []string `json:"categories"`
// Inference backend version and parameters
@@ -1,7 +1,6 @@
package llm_container
import (
"encoding/json"
"fmt"
"strings"
)
@@ -11,15 +10,10 @@ type runtimeArg struct {
Value string
}
func parseBackendParameterArgs(raw string, validate func(string) error) ([]runtimeArg, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
func parseBackendParameterArgs(items []string, validate func(string) error) ([]runtimeArg, error) {
if len(items) == 0 {
return nil, nil
}
items := []string{}
if err := json.Unmarshal([]byte(raw), &items); err != nil {
items = []string{raw}
}
args := make([]runtimeArg, 0, len(items))
errs := make([]string, 0)
for _, item := range items {
@@ -0,0 +1,46 @@
package llm_container
import (
"strings"
"testing"
api "yunion.io/x/onecloud/pkg/apis/llm"
"yunion.io/x/onecloud/pkg/llm/models"
)
func TestBuildVLLMEntrypointScriptMountedModelsFlag(t *testing.T) {
sleepScript := buildVLLMEntrypointScript(false, 1, nil, nil)
if !strings.Contains(sleepScript, "sleep infinity") {
t.Fatalf("expected idle script without mounted models, got %q", sleepScript)
}
serveScript := buildVLLMEntrypointScript(true, 1, nil, &api.LLMSpecVllm{PreferredModel: "Qwen3-8B"})
if strings.Contains(serveScript, "sleep infinity") {
t.Fatalf("expected serve script with mounted models, got %q", serveScript)
}
if !strings.Contains(serveScript, api.LLM_VLLM_EXEC_PATH) {
t.Fatalf("expected vllm exec in serve script, got %q", serveScript)
}
}
func TestLocalPathSkuEnablesServeEntrypoint(t *testing.T) {
hostPaths := api.HostPaths{
{
Type: "directory",
Path: "/data/models/Qwen3-8B",
Containers: api.ContainerHostPathRelations{
"0": {MountPath: "/data/models/huggingface/Qwen3-8B"},
},
},
}
sku := &models.SLLMSku{
LLMType: string(api.LLM_CONTAINER_VLLM),
Source: api.LLM_MODEL_SOURCE_LOCAL_PATH,
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")
}
}
+6 -6
View File
@@ -222,11 +222,11 @@ func (s *sglang) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *
if eff := s.GetEffectiveSpec(llm, sku); eff != nil {
effSpec = eff.(*api.LLMSpecSGLang)
}
backendParameters := ""
var backendParameters []string
if sku != nil {
backendParameters = sku.BackendParameters
}
hasMountedModels := len(postOverlays) > 0
hasMountedModels := len(postOverlays) > 0 || models.SkuHasLocalHostPathModel(sku)
startScript := buildSGLangEntrypointScript(hasMountedModels, tensorParallelSize, backendParameters, effSpec)
envs := []*commonapi.ContainerKeyValue{
{
@@ -637,7 +637,7 @@ func sglangCustomizedArgsToRuntime(args []*api.SGLangCustomizedArg) []runtimeArg
return out
}
func appendSGLangRuntimeFlags(flags []string, backendParameters string, effSpec *api.LLMSpecSGLang) []string {
func appendSGLangRuntimeFlags(flags []string, backendParameters []string, effSpec *api.LLMSpecSGLang) []string {
backendArgs, err := parseBackendParameterArgs(backendParameters, validateSGLangArgKey)
if err != nil {
log.Errorf("parse sglang backend parameters: %v", err)
@@ -654,7 +654,7 @@ func appendSGLangRuntimeFlags(flags []string, backendParameters string, effSpec
return appendRuntimeFlags(flags, mergedArgs)
}
func buildSGLangServeFlagsWithModelExpr(modelExpr string, servedModelNameExpr string, tensorParallelSize int, backendParameters string, effSpec *api.LLMSpecSGLang) []string {
func buildSGLangServeFlagsWithModelExpr(modelExpr string, servedModelNameExpr string, tensorParallelSize int, backendParameters []string, effSpec *api.LLMSpecSGLang) []string {
flags := []string{
fmt.Sprintf("--model-path %s", modelExpr),
fmt.Sprintf("--served-model-name %s", servedModelNameExpr),
@@ -665,7 +665,7 @@ func buildSGLangServeFlagsWithModelExpr(modelExpr string, servedModelNameExpr st
return appendSGLangRuntimeFlags(flags, backendParameters, effSpec)
}
func buildSGLangServeFlags(modelPath string, tensorParallelSize int, backendParameters string, effSpec *api.LLMSpecSGLang) []string {
func buildSGLangServeFlags(modelPath string, tensorParallelSize int, backendParameters []string, effSpec *api.LLMSpecSGLang) []string {
modelQuoted := shellQuoteSingle(modelPath)
return buildSGLangServeFlagsWithModelExpr(
modelQuoted,
@@ -676,7 +676,7 @@ func buildSGLangServeFlags(modelPath string, tensorParallelSize int, backendPara
)
}
func buildSGLangEntrypointScript(hasMountedModels bool, tensorParallelSize int, backendParameters string, effSpec *api.LLMSpecSGLang) string {
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)
+6 -6
View File
@@ -164,7 +164,7 @@ func vllmCustomizedArgsToRuntime(args []*api.VllmCustomizedArg) []runtimeArg {
return out
}
func appendVLLMRuntimeFlags(flags []string, backendParameters string, effSpec *api.LLMSpecVllm) []string {
func appendVLLMRuntimeFlags(flags []string, backendParameters []string, effSpec *api.LLMSpecVllm) []string {
backendArgs, err := parseBackendParameterArgs(backendParameters, validateVLLMArgKey)
if err != nil {
log.Errorf("parse vllm backend parameters: %v", err)
@@ -181,7 +181,7 @@ func appendVLLMRuntimeFlags(flags []string, backendParameters string, effSpec *a
return appendRuntimeFlags(flags, mergedArgs)
}
func buildVLLMServeFlagsWithModelExpr(modelExpr string, servedModelNameExpr string, tensorParallelSize int, backendParameters string, effSpec *api.LLMSpecVllm) []string {
func buildVLLMServeFlagsWithModelExpr(modelExpr string, servedModelNameExpr string, tensorParallelSize int, backendParameters []string, effSpec *api.LLMSpecVllm) []string {
flags := []string{
fmt.Sprintf("--model %s", modelExpr),
fmt.Sprintf("--served-model-name %s", servedModelNameExpr),
@@ -191,7 +191,7 @@ func buildVLLMServeFlagsWithModelExpr(modelExpr string, servedModelNameExpr stri
return appendVLLMRuntimeFlags(flags, backendParameters, effSpec)
}
func buildVLLMServeFlags(modelPath string, tensorParallelSize int, backendParameters string, effSpec *api.LLMSpecVllm) []string {
func buildVLLMServeFlags(modelPath string, tensorParallelSize int, backendParameters []string, effSpec *api.LLMSpecVllm) []string {
modelQuoted := shellQuoteSingle(modelPath)
return buildVLLMServeFlagsWithModelExpr(
modelQuoted,
@@ -202,7 +202,7 @@ func buildVLLMServeFlags(modelPath string, tensorParallelSize int, backendParame
)
}
func buildVLLMEntrypointScript(hasMountedModels bool, tensorParallelSize int, backendParameters string, effSpec *api.LLMSpecVllm) string {
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)
@@ -430,11 +430,11 @@ func (v *vllm) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *mo
if eff := v.GetEffectiveSpec(llm, sku); eff != nil {
effSpec = eff.(*api.LLMSpecVllm)
}
backendParameters := ""
var backendParameters []string
if sku != nil {
backendParameters = sku.BackendParameters
}
hasMountedModels := len(postOverlays) > 0
hasMountedModels := len(postOverlays) > 0 || models.SkuHasLocalHostPathModel(sku)
startScript := buildVLLMEntrypointScript(hasMountedModels, tensorParallelSize, backendParameters, effSpec)
envs := []*commonapi.ContainerKeyValue{
{
+15 -4
View File
@@ -138,12 +138,20 @@ func skuFromLLMSkuCreateInput(input *api.LLMSkuCreateInput) *SLLMSku {
if input == nil {
return nil
}
return &SLLMSku{
LLMType: input.LLMType,
sku := &SLLMSku{
LLMType: input.LLMType,
Source: input.Source,
LocalPath: input.LocalPath,
SLLMSkuBase: SLLMSkuBase{
Devices: input.Devices,
Devices: input.Devices,
VramClaimMb: input.VramClaimMb,
HostPaths: input.HostPaths,
},
}
if len(input.PreferHosts) > 0 {
sku.PreferHosts = append([]string(nil), input.PreferHosts...)
}
return sku
}
// ValidateRequireMountedModels errors if neither input nor existing llm nor sku supplies mounted_models. For create, pass nil/empty for llmCurMountedModels.
@@ -153,6 +161,9 @@ func ValidateRequireMountedModels(
llmCurMountedModels []string,
sku *SLLMSku,
) error {
if sku != nil && SkuHasLocalHostPathModel(sku) {
return nil
}
effectiveModels := llmCurMountedModels
if inputMountedModels != nil {
effectiveModels = inputMountedModels
@@ -715,7 +726,7 @@ func (llm *SLLM) ServerCreate(ctx context.Context, userCred mcclient.TokenCreden
if nil != err {
return "", errors.Wrap(err, "GetPodCreateInput")
}
log.Infoln("PodCreateInput Data: ", jsonutils.Marshal(data).String())
log.Infof("PodCreateInput Data: %s", jsonutils.Marshal(data).String())
resp, err := compute.Servers.Create(s, jsonutils.Marshal(data))
if nil != err {
+88 -25
View File
@@ -604,38 +604,100 @@ func computeAiproxyBindingSyncResult(bindings []api.AiproxyInstanceBinding, runn
return aiproxyBindingSyncSynced
}
func resolveDeploymentStatusAfterAiproxySync(dep *SLLMDeployment, result aiproxyBindingSyncResult) string {
func resolveAiproxySyncStatusAfterReconcile(result aiproxyBindingSyncResult) string {
switch result {
case aiproxyBindingSyncSynced:
if dep.Replicas > 0 && dep.ReadyReplicas >= dep.Replicas {
return api.STATUS_READY
}
if dep.ReadyReplicas > 0 {
return api.LLM_DEPLOYMENT_STATUS_PARTIAL
}
return api.LLM_DEPLOYMENT_STATUS_AIPROXY_PENDING
return api.AIPROXY_SYNC_STATUS_SYNCED
case aiproxyBindingSyncPartial:
return api.LLM_DEPLOYMENT_STATUS_AIPROXY_PARTIAL
return api.AIPROXY_SYNC_STATUS_PARTIAL
case aiproxyBindingSyncFailed:
return api.LLM_DEPLOYMENT_STATUS_AIPROXY_SYNC_FAILED
return api.AIPROXY_SYNC_STATUS_FAILED
default:
return api.LLM_DEPLOYMENT_STATUS_AIPROXY_PENDING
return api.AIPROXY_SYNC_STATUS_PENDING
}
}
func deploymentStatusMessageAfterAiproxySync(dep *SLLMDeployment, result aiproxyBindingSyncResult) string {
func aiproxySyncStatusMessage(dep *SLLMDeployment, result aiproxyBindingSyncResult) string {
switch result {
case aiproxyBindingSyncSynced:
return fmt.Sprintf("aiproxy synced, ready_replicas=%d/%d", dep.ReadyReplicas, dep.Replicas)
case aiproxyBindingSyncPartial:
return fmt.Sprintf("aiproxy partially synced, ready_replicas=%d/%d", dep.ReadyReplicas, dep.Replicas)
msg := fmt.Sprintf("aiproxy partially synced, ready_replicas=%d/%d", dep.ReadyReplicas, dep.Replicas)
if reason := AiproxySyncFailureReason(dep); reason != "" {
return msg + ": " + reason
}
return msg
case aiproxyBindingSyncFailed:
if reason := AiproxySyncFailureReason(dep); reason != "" {
return reason
}
return "aiproxy sync failed"
default:
return "waiting for running replicas"
}
}
// AiproxySyncFailureReason summarizes per-replica binding errors for logs and UI.
func AiproxySyncFailureReason(dep *SLLMDeployment) string {
if dep == nil {
return ""
}
bindings := deploymentAiproxyBindings(dep)
if len(bindings) == 0 {
return ""
}
parts := make([]string, 0, len(bindings))
for i := range bindings {
b := bindings[i]
if b.SyncStatus != api.AIPROXY_BINDING_SYNC_FAILED {
continue
}
errMsg := strings.TrimSpace(b.LastError)
if errMsg == "" {
errMsg = "unknown error"
}
if b.LlmId != "" {
parts = append(parts, fmt.Sprintf("llm %s: %s", b.LlmId, errMsg))
} else {
parts = append(parts, errMsg)
}
}
return strings.Join(parts, "; ")
}
func (dep *SLLMDeployment) SetAiproxySyncStatus(ctx context.Context, userCred mcclient.TokenCredential, status, msg string) error {
if dep == nil {
return errors.Wrap(httperrors.ErrInvalidStatus, "nil deployment")
}
if status == "" {
status = api.AIPROXY_SYNC_STATUS_DISABLED
}
if dep.AiproxySyncStatus == status {
return nil
}
if _, err := db.Update(dep, func() error {
dep.AiproxySyncStatus = status
return nil
}); err != nil {
return errors.Wrap(err, "update aiproxy_sync_status")
}
if msg != "" {
db.OpsLog.LogEvent(dep, "aiproxy_sync", msg, userCred)
}
return nil
}
func inferAiproxySyncStatus(dep *SLLMDeployment) string {
if dep == nil || !dep.AutoRegisterAiproxy {
return api.AIPROXY_SYNC_STATUS_DISABLED
}
bindings := deploymentAiproxyBindings(dep)
if len(bindings) == 0 {
return api.AIPROXY_SYNC_STATUS_PENDING
}
return resolveAiproxySyncStatusAfterReconcile(computeAiproxyBindingSyncResult(bindings, dep.ReadyReplicas))
}
// ReconcileDeploymentAiproxy syncs all running replicas and refreshes routing bindings.
func ReconcileDeploymentAiproxy(ctx context.Context, userCred mcclient.TokenCredential, dep *SLLMDeployment) error {
if dep == nil {
@@ -645,7 +707,7 @@ func ReconcileDeploymentAiproxy(ctx context.Context, userCred mcclient.TokenCred
return nil
}
if err := dep.SetStatus(ctx, userCred, api.LLM_DEPLOYMENT_STATUS_AIPROXY_SYNCING, "aiproxy sync in progress"); err != nil {
if err := dep.SetAiproxySyncStatus(ctx, userCred, api.AIPROXY_SYNC_STATUS_SYNCING, "aiproxy sync in progress"); err != nil {
return errors.Wrap(err, "set aiproxy syncing status")
}
@@ -658,7 +720,7 @@ func ReconcileDeploymentAiproxy(ctx context.Context, userCred mcclient.TokenCred
if err := persistDeploymentAiproxyBindings(dep, dep.AiproxyRoutingId, nil); err != nil {
return err
}
return dep.SetStatus(ctx, userCred, api.LLM_DEPLOYMENT_STATUS_AIPROXY_PENDING, "waiting for running replicas")
return dep.SetAiproxySyncStatus(ctx, userCred, api.AIPROXY_SYNC_STATUS_PENDING, "waiting for running replicas")
}
session := aiproxyAdminSession(ctx)
@@ -667,19 +729,19 @@ func ReconcileDeploymentAiproxy(ctx context.Context, userCred mcclient.TokenCred
items, bindings, err := buildRoutingModelItems(ctx, userCred, dep, llms)
if err != nil {
_ = persistDeploymentAiproxyBindings(dep, dep.AiproxyRoutingId, bindings)
_ = dep.SetStatus(ctx, userCred, api.LLM_DEPLOYMENT_STATUS_AIPROXY_SYNC_FAILED, err.Error())
_ = dep.SetAiproxySyncStatus(ctx, userCred, api.AIPROXY_SYNC_STATUS_FAILED, err.Error())
return err
}
primaryModelKey := primaryUpstreamModelKeyFromBindings(ctx, userCred, bindings)
routingId, err := ensureAiRouting(session, routingName, dep, deploymentRoutingModelKey(dep, primaryModelKey))
if err != nil {
_ = persistDeploymentAiproxyBindings(dep, dep.AiproxyRoutingId, bindings)
_ = dep.SetStatus(ctx, userCred, api.LLM_DEPLOYMENT_STATUS_AIPROXY_SYNC_FAILED, err.Error())
_ = dep.SetAiproxySyncStatus(ctx, userCred, api.AIPROXY_SYNC_STATUS_FAILED, err.Error())
return err
}
if err := applyRoutingModels(session, routingId, items); err != nil {
_ = persistDeploymentAiproxyBindings(dep, routingId, bindings)
_ = dep.SetStatus(ctx, userCred, api.LLM_DEPLOYMENT_STATUS_AIPROXY_SYNC_FAILED, err.Error())
_ = dep.SetAiproxySyncStatus(ctx, userCred, api.AIPROXY_SYNC_STATUS_FAILED, err.Error())
return err
}
@@ -687,8 +749,8 @@ func ReconcileDeploymentAiproxy(ctx context.Context, userCred mcclient.TokenCred
if err := persistDeploymentAiproxyBindings(dep, routingId, bindings); err != nil {
return err
}
status := resolveDeploymentStatusAfterAiproxySync(dep, result)
return dep.SetStatus(ctx, userCred, status, deploymentStatusMessageAfterAiproxySync(dep, result))
syncStatus := resolveAiproxySyncStatusAfterReconcile(result)
return dep.SetAiproxySyncStatus(ctx, userCred, syncStatus, aiproxySyncStatusMessage(dep, result))
}
func deleteAiProviderById(session *mcclient.ClientSession, providerId string) error {
@@ -812,7 +874,7 @@ func UnsyncLlmInstance(ctx context.Context, userCred mcclient.TokenCredential, d
if err := persistDeploymentAiproxyBindings(dep, "", nil); err != nil {
return err
}
return dep.SetStatus(ctx, userCred, api.LLM_DEPLOYMENT_STATUS_AIPROXY_PENDING, "waiting for running replicas")
return dep.SetAiproxySyncStatus(ctx, userCred, api.AIPROXY_SYNC_STATUS_PENDING, "waiting for running replicas")
}
if len(remaining) == 0 {
@@ -822,7 +884,7 @@ func UnsyncLlmInstance(ctx context.Context, userCred mcclient.TokenCredential, d
if err := persistDeploymentAiproxyBindings(dep, "", nil); err != nil {
return err
}
return dep.SetStatus(ctx, userCred, api.LLM_DEPLOYMENT_STATUS_AIPROXY_PENDING, "waiting for running replicas")
return dep.SetAiproxySyncStatus(ctx, userCred, api.AIPROXY_SYNC_STATUS_PENDING, "waiting for running replicas")
}
items := make([]apapi.AiRoutingModelItem, 0, len(remaining))
@@ -870,8 +932,8 @@ func UnsyncLlmInstance(ctx context.Context, userCred mcclient.TokenCredential, d
if err := persistDeploymentAiproxyBindings(dep, routingId, bindings); err != nil {
return err
}
status := resolveDeploymentStatusAfterAiproxySync(dep, result)
return dep.SetStatus(ctx, userCred, status, deploymentStatusMessageAfterAiproxySync(dep, result))
syncStatus := resolveAiproxySyncStatusAfterReconcile(result)
return dep.SetAiproxySyncStatus(ctx, userCred, syncStatus, aiproxySyncStatusMessage(dep, result))
}
func parseBindingForLlm(dep *SLLMDeployment, llmId string) (api.AiproxyInstanceBinding, error) {
@@ -909,10 +971,11 @@ func clearDeploymentAiproxyRegistrationState(dep *SLLMDeployment) {
dep.AutoRegisterAiproxy = false
dep.AiproxyRoutingId = ""
dep.AiproxyBindings = nil
dep.AiproxySyncStatus = api.AIPROXY_SYNC_STATUS_DISABLED
}
func (dep *SLLMDeployment) StartAiproxySyncTask(ctx context.Context, userCred mcclient.TokenCredential, llmId, parentTaskId string) error {
if dep.Status == api.LLM_DEPLOYMENT_STATUS_AIPROXY_SYNCING && parentTaskId == "" {
if dep.AiproxySyncStatus == api.AIPROXY_SYNC_STATUS_SYNCING && parentTaskId == "" {
return nil
}
params := jsonutils.NewDict()
+33 -16
View File
@@ -89,6 +89,7 @@ func TestClearDeploymentAiproxyRegistrationState(t *testing.T) {
dep.AutoRegisterAiproxy = true
dep.AiproxyRoutingId = "routing-1"
dep.AiproxyBindings = &api.AiproxyBindings{{LlmId: "llm-1"}}
dep.AiproxySyncStatus = api.AIPROXY_SYNC_STATUS_SYNCED
clearDeploymentAiproxyRegistrationState(dep)
@@ -101,48 +102,64 @@ func TestClearDeploymentAiproxyRegistrationState(t *testing.T) {
if dep.AiproxyBindings != nil {
t.Fatal("AiproxyBindings should be nil")
}
if dep.AiproxySyncStatus != api.AIPROXY_SYNC_STATUS_DISABLED {
t.Fatalf("AiproxySyncStatus should be disabled, got %q", dep.AiproxySyncStatus)
}
}
func TestResolveDeploymentStatusAfterAiproxySync(t *testing.T) {
func TestResolveAiproxySyncStatusAfterReconcile(t *testing.T) {
cases := []struct {
name string
dep SLLMDeployment
result aiproxyBindingSyncResult
wantStat string
}{
{
name: "fully synced all replicas",
dep: SLLMDeployment{Replicas: 2, ReadyReplicas: 2},
name: "fully synced",
result: aiproxyBindingSyncSynced,
wantStat: api.STATUS_READY,
},
{
name: "synced partial replicas",
dep: SLLMDeployment{Replicas: 2, ReadyReplicas: 1},
result: aiproxyBindingSyncSynced,
wantStat: api.LLM_DEPLOYMENT_STATUS_PARTIAL,
wantStat: api.AIPROXY_SYNC_STATUS_SYNCED,
},
{
name: "binding partial failure",
dep: SLLMDeployment{Replicas: 2, ReadyReplicas: 2},
result: aiproxyBindingSyncPartial,
wantStat: api.LLM_DEPLOYMENT_STATUS_AIPROXY_PARTIAL,
wantStat: api.AIPROXY_SYNC_STATUS_PARTIAL,
},
{
name: "all bindings failed",
dep: SLLMDeployment{Replicas: 1, ReadyReplicas: 1},
result: aiproxyBindingSyncFailed,
wantStat: api.LLM_DEPLOYMENT_STATUS_AIPROXY_SYNC_FAILED,
wantStat: api.AIPROXY_SYNC_STATUS_FAILED,
},
{
name: "pending",
result: aiproxyBindingSyncPending,
wantStat: api.AIPROXY_SYNC_STATUS_PENDING,
},
}
for _, c := range cases {
got := resolveDeploymentStatusAfterAiproxySync(&c.dep, c.result)
got := resolveAiproxySyncStatusAfterReconcile(c.result)
if got != c.wantStat {
t.Fatalf("%s: got %q want %q", c.name, got, c.wantStat)
}
}
}
func TestAiproxySyncFailureReason(t *testing.T) {
dep := &SLLMDeployment{}
dep.AiproxyBindings = &api.AiproxyBindings{
{LlmId: "llm-1", SyncStatus: api.AIPROXY_BINDING_SYNC_SYNCED},
{LlmId: "llm-2", SyncStatus: api.AIPROXY_BINDING_SYNC_FAILED, LastError: "provider upsert failed"},
}
got := AiproxySyncFailureReason(dep)
want := "llm llm-2: provider upsert failed"
if got != want {
t.Fatalf("AiproxySyncFailureReason() = %q, want %q", got, want)
}
msg := aiproxySyncStatusMessage(dep, aiproxyBindingSyncFailed)
if msg != want {
t.Fatalf("aiproxySyncStatusMessage(failed) = %q, want %q", msg, want)
}
}
func TestUpstreamModelKeyForBackend(t *testing.T) {
cases := []struct {
llmType string
+6 -22
View File
@@ -127,29 +127,13 @@ func (man *SLLMBaseManager) ValidateCreateData(ctx context.Context, userCred mcc
return input, errors.Wrap(err, "validate VirtualResourceCreateInput")
}
/*
if len(input.PreferHost) > 0 {
s := auth.GetSession(ctx, userCred, "")
hostJson, err := compute.Hosts.Get(s, input.PreferHost, nil)
if err != nil {
return input, errors.Wrap(err, "get host")
}
hostDetails := computeapi.HostDetails{}
if err := hostJson.Unmarshal(&hostDetails); err != nil {
return input, errors.Wrap(err, "unmarshal hostDetails")
}
if hostDetails.Enabled == nil || !*hostDetails.Enabled {
return input, errors.Wrap(errors.ErrInvalidStatus, "not enabled")
}
if hostDetails.HostStatus != computeapi.HOST_ONLINE {
return input, errors.Wrap(errors.ErrInvalidStatus, "not online")
}
if hostDetails.HostType != computeapi.HOST_TYPE_CONTAINER {
return input, errors.Wrapf(httperrors.ErrNotAcceptable, "host_type %s not supported", hostDetails.HostType)
}
input.PreferHost = hostDetails.Id
if len(input.PreferHost) > 0 {
resolved, err := resolvePreferHost(ctx, userCred, input.PreferHost)
if err != nil {
return input, errors.Wrap(err, "resolve prefer_host")
}
*/
input.PreferHost = resolved
}
// 处理网络配置
var firstNet *computeapi.NetworkConfig
+202 -24
View File
@@ -99,15 +99,18 @@ type SLLMDeployment struct {
// Instance template — captured at create time so scale-up can re-create
// new SLLM replicas with the same network / start / mount settings as the originals.
// Scale request bodies don't carry these fields.
Nets *api.LLMDeploymentNets `charset:"utf8" length:"medium" nullable:"true" list:"user"`
AutoStart bool `nullable:"false" default:"false" list:"user"`
HostPaths *api.HostPaths `charset:"utf8" length:"medium" nullable:"true" list:"user"`
Nets *api.LLMDeploymentNets `charset:"utf8" length:"medium" nullable:"true" list:"user"`
AutoStart bool `nullable:"false" default:"false" list:"user"`
HostPaths *api.HostPaths `charset:"utf8" length:"medium" nullable:"true" list:"user"`
PreferHosts []string `charset:"utf8" length:"medium" nullable:"true" list:"user" create:"optional" update:"user"`
// Aiproxy integration (llm sync writes aiproxy catalog via mcclient).
AutoRegisterAiproxy bool `nullable:"false" default:"false" list:"user" create:"optional" update:"user"`
AiproxyModelPrefix string `width:"128" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
AiproxyRoutingId string `width:"128" charset:"ascii" nullable:"true" list:"user"`
AiproxyBindings *api.AiproxyBindings `charset:"utf8" length:"long" nullable:"true" list:"user"`
// Gateway registration/sync progress (independent from replica-health status).
AiproxySyncStatus string `width:"32" charset:"ascii" nullable:"false" default:"disabled" list:"user"`
}
func (man *SLLMDeploymentManager) ValidateCreateData(
@@ -145,7 +148,11 @@ func (man *SLLMDeploymentManager) ValidateCreateData(
if err := validateDeploymentGpuMemoryUtilization(input.GpuMemoryUtilization, input.AutoGpuMemoryUtilization, lSku.LLMType); err != nil {
return input, err
}
defaultDeploymentAutoGpuMemoryUtilization(input, lSku.LLMType)
defaultDeploymentAutoGpuMemoryUtilization(input, lSku, lSku.LLMType)
disableDeploymentAutoGpuMemoryUtilizationForLocalPath(input, lSku)
if err := validateLocalPathDeploymentPreferHosts(ctx, userCred, input, lSku); err != nil {
return input, err
}
if err := ValidateDeploymentDevices(lSku.LLMType, lSku); err != nil {
return input, err
}
@@ -174,11 +181,16 @@ func (man *SLLMDeploymentManager) ValidateCreateData(
input.ModelSpec.LlmType = api.LLMContainerType(input.SkuSpec.LLMType)
}
}
skuInput := skuFromLLMSkuCreateInput(input.SkuSpec)
if err := validateDeploymentGpuMemoryUtilization(input.GpuMemoryUtilization, input.AutoGpuMemoryUtilization, input.SkuSpec.LLMType); err != nil {
return input, err
}
defaultDeploymentAutoGpuMemoryUtilization(input, input.SkuSpec.LLMType)
if err := ValidateDeploymentDevices(input.SkuSpec.LLMType, skuFromLLMSkuCreateInput(input.SkuSpec)); err != nil {
defaultDeploymentAutoGpuMemoryUtilization(input, skuInput, input.SkuSpec.LLMType)
disableDeploymentAutoGpuMemoryUtilizationForLocalPath(input, skuInput)
if err := validateLocalPathDeploymentPreferHosts(ctx, userCred, input, skuInput); err != nil {
return input, err
}
if err := ValidateDeploymentDevices(input.SkuSpec.LLMType, skuInput); err != nil {
return input, err
}
}
@@ -327,6 +339,8 @@ func (man *SLLMDeploymentManager) FetchCustomizeColumns(
res[i].AutoRegisterAiproxy = models[i].AutoRegisterAiproxy
res[i].AiproxyModelPrefix = models[i].AiproxyModelPrefix
res[i].AiproxyRoutingId = models[i].AiproxyRoutingId
res[i].AiproxySyncStatus = models[i].AiproxySyncStatus
res[i].PreferHosts = models[i].PreferHosts
}
// Batch fetch SKU data for source/backend/categories info
@@ -416,13 +430,15 @@ type SyncReadyReplicasOptions struct {
// to the deployment row, and transitions the deployment status based on replica
// health:
//
// ready_replicas == 0 && desired > 0 → deploying
// ready_replicas == 0 && desired > 0 → deploying (or create_fail / start_fail)
// 0 < ready_replicas < desired → partial
// ready_replicas == desired → ready
// replica create/start failure → create_fail (unless some replicas run)
// replica create_fail with 0 running → create_fail
// replica start_fail with 0 running → start_fail
//
// Failure / lifecycle statuses (create_fail, deleting, importing_model, etc.)
// are not overridden once set.
// Failure / lifecycle statuses (deleting, importing_model, etc.) are not
// overridden once set. create_fail and start_fail may recover to ready/partial
// when replicas become healthy again (e.g. after syncstatus or auto-restart).
//
// Call after create/scale tasks finish and on every instance status change.
func (model *SLLMDeployment) SyncReadyReplicas(ctx context.Context, userCred mcclient.TokenCredential, opts ...SyncReadyReplicasOptions) error {
@@ -451,7 +467,7 @@ func (model *SLLMDeployment) SyncReadyReplicas(ctx context.Context, userCred mcc
if desired == "" {
return nil
}
if !canUpdateReplicaHealthStatus(model.Status) {
if !canUpdateReplicaHealthStatus(model.Status, desired) {
return nil
}
oldStatus := model.Status
@@ -483,8 +499,10 @@ type deploymentReplicaStatusRow struct {
}
type deploymentReplicaStatusSummary struct {
Running int
HasFailure bool
Running int
HasCreateFailure bool
HasStartFailure bool
HasProbing bool
}
func summarizeDeploymentReplicaStatuses(rows []deploymentReplicaStatusRow) deploymentReplicaStatusSummary {
@@ -493,10 +511,12 @@ func summarizeDeploymentReplicaStatuses(rows []deploymentReplicaStatusRow) deplo
switch rows[i].Status {
case api.LLM_STATUS_RUNNING:
summary.Running++
default:
if isDeploymentReplicaFailureStatus(rows[i].Status) {
summary.HasFailure = true
}
case api.LLM_STATUS_CREATE_FAIL:
summary.HasCreateFailure = true
case api.LLM_STATUS_START_FAIL:
summary.HasStartFailure = true
case api.LLM_STATUS_PROBING:
summary.HasProbing = true
}
}
return summary
@@ -511,8 +531,12 @@ func computeDeploymentReplicaStatus(summary deploymentReplicaStatusSummary, desi
return api.STATUS_READY
case summary.Running > 0:
return api.LLM_DEPLOYMENT_STATUS_PARTIAL
case summary.HasFailure:
case summary.HasCreateFailure:
return api.LLM_STATUS_CREATE_FAIL
case summary.HasStartFailure:
return api.LLM_STATUS_START_FAIL
case summary.HasProbing:
return api.LLM_DEPLOYMENT_STATUS_DEPLOYING
default:
return api.LLM_DEPLOYMENT_STATUS_DEPLOYING
}
@@ -529,17 +553,22 @@ func isDeploymentReplicaFailureStatus(status string) bool {
// canUpdateReplicaHealthStatus reports whether replica-health-driven status can
// override the current deployment value. Early lifecycle and terminal failure /
// delete states must not be clobbered.
func canUpdateReplicaHealthStatus(current string) bool {
// delete states must not be clobbered, except create_fail and start_fail may
// recover when replicas are running again.
func canUpdateReplicaHealthStatus(current, desired string) bool {
if current == api.LLM_STATUS_CREATE_FAIL {
return desired == api.STATUS_READY || desired == api.LLM_DEPLOYMENT_STATUS_PARTIAL
}
if current == api.LLM_STATUS_START_FAIL {
return desired == api.STATUS_READY || desired == api.LLM_DEPLOYMENT_STATUS_PARTIAL
}
switch current {
case api.LLM_DEPLOYMENT_STATUS_IMPORTING_MODEL,
api.LLM_DEPLOYMENT_STATUS_IMPORT_MODEL_FAILED,
api.LLM_DEPLOYMENT_STATUS_CREATING_SKU,
api.LLM_DEPLOYMENT_STATUS_CREATE_SKU_FAILED,
api.LLM_STATUS_CREATE_FAIL,
api.LLM_STATUS_DELETING,
api.LLM_STATUS_DELETE_FAILED,
api.LLM_DEPLOYMENT_STATUS_AIPROXY_SYNCING:
api.LLM_STATUS_DELETE_FAILED:
return false
}
return true
@@ -597,12 +626,20 @@ func (model *SLLMDeployment) PostCreate(ctx context.Context, userCred mcclient.T
if input.HostPaths != nil && !input.HostPaths.IsZero() {
model.HostPaths = input.HostPaths
}
if len(input.PreferHosts) > 0 {
model.PreferHosts = append([]string(nil), input.PreferHosts...)
}
if input.AutoRegisterAiproxy != nil {
model.AutoRegisterAiproxy = *input.AutoRegisterAiproxy
}
if input.AiproxyModelPrefix != "" {
model.AiproxyModelPrefix = strings.TrimSpace(input.AiproxyModelPrefix)
}
if model.AutoRegisterAiproxy {
model.AiproxySyncStatus = api.AIPROXY_SYNC_STATUS_PENDING
} else {
model.AiproxySyncStatus = api.AIPROXY_SYNC_STATUS_DISABLED
}
return nil
}); err != nil {
log.Errorf("SLLMDeployment.PostCreate persist instance template: %s", err)
@@ -628,7 +665,7 @@ func (model *SLLMDeployment) StartCreateTask(ctx context.Context, userCred mccli
}
func (model *SLLMDeployment) StartSyncReplicasTask(ctx context.Context, userCred mcclient.TokenCredential, data jsonutils.JSONObject) error {
model.SetStatus(ctx, userCred, "syncing", "")
model.SetStatus(ctx, userCred, api.LLM_DEPLOYMENT_STATUS_SYNCING, "")
params, _ := data.(*jsonutils.JSONDict)
if params == nil {
params = jsonutils.NewDict()
@@ -667,6 +704,7 @@ func (model *SLLMDeployment) PerformRegisterAiproxy(
) (jsonutils.JSONObject, error) {
if _, err := db.Update(model, func() error {
model.AutoRegisterAiproxy = true
model.AiproxySyncStatus = api.AIPROXY_SYNC_STATUS_PENDING
return nil
}); err != nil {
return nil, errors.Wrap(err, "enable auto_register_aiproxy")
@@ -691,6 +729,146 @@ func (model *SLLMDeployment) PerformUnregisterAiproxy(
return nil, nil
}
// canRestartDeploymentStatus reports whether a deployment may be restarted.
func canRestartDeploymentStatus(status string) bool {
switch status {
case api.STATUS_READY,
api.LLM_DEPLOYMENT_STATUS_PARTIAL,
api.LLM_STATUS_RUNNING:
return true
}
return false
}
func (model *SLLMDeployment) ValidateRestartInput(
ctx context.Context,
userCred mcclient.TokenCredential,
input *api.LLMDeploymentRestartInput,
) error {
if !canRestartDeploymentStatus(model.Status) {
return httperrors.NewInvalidStatusError("invalid deployment status %s", model.Status)
}
var rows []struct {
Id string `json:"id"`
}
err := GetLLMManager().Query("id").Equals("llm_deployment_id", model.Id).All(&rows)
if err != nil {
return errors.Wrap(err, "fetch deployment instances")
}
if len(rows) == 0 {
return httperrors.NewInvalidStatusError("no instances under deployment")
}
restartable := 0
for i := range rows {
llmObj, err := GetLLMManager().FetchById(rows[i].Id)
if err != nil {
continue
}
llm := llmObj.(*SLLM)
if _, err := llm.ValidateRestartInput(ctx, userCred, &api.LLMRestartInput{}); err == nil {
restartable++
}
}
if restartable == 0 {
return httperrors.NewInvalidStatusError("no restartable instances")
}
return nil
}
func (model *SLLMDeployment) PerformRestart(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
input *api.LLMDeploymentRestartInput,
) (jsonutils.JSONObject, error) {
if err := model.ValidateRestartInput(ctx, userCred, input); err != nil {
return nil, err
}
if err := model.StartRestartTask(ctx, userCred); err != nil {
return nil, errors.Wrap(err, "StartRestartTask")
}
return nil, nil
}
func (model *SLLMDeployment) StartRestartTask(ctx context.Context, userCred mcclient.TokenCredential) error {
task, err := taskman.TaskManager.NewTask(ctx, "LLMDeploymentRestartTask", model, userCred, nil, "", "", nil)
if err != nil {
return errors.Wrap(err, "NewTask LLMDeploymentRestartTask")
}
return task.ScheduleRun(nil)
}
// canSyncDeploymentStatus reports whether a deployment may sync replica statuses.
func canSyncDeploymentStatus(status string) bool {
switch status {
case api.LLM_STATUS_DELETING,
api.LLM_STATUS_START_DELETE,
api.LLM_STATUS_DELETED,
api.LLM_STATUS_DELETE_FAILED,
"creating",
api.LLM_DEPLOYMENT_STATUS_IMPORTING_MODEL,
api.LLM_DEPLOYMENT_STATUS_CREATING_SKU,
api.LLM_DEPLOYMENT_STATUS_SYNCING:
return false
}
return true
}
func (model *SLLMDeployment) ValidateSyncstatusInput(
ctx context.Context,
userCred mcclient.TokenCredential,
input *api.LLMDeploymentSyncstatusInput,
) error {
if !canSyncDeploymentStatus(model.Status) {
return httperrors.NewInvalidStatusError("invalid deployment status %s", model.Status)
}
var rows []struct {
Id string `json:"id"`
CmpId string `json:"cmp_id"`
}
err := GetLLMManager().Query("id", "cmp_id").Equals("llm_deployment_id", model.Id).All(&rows)
if err != nil {
return errors.Wrap(err, "fetch deployment instances")
}
if len(rows) == 0 {
return httperrors.NewInvalidStatusError("no instances under deployment")
}
syncable := 0
for i := range rows {
if rows[i].CmpId != "" {
syncable++
}
}
if syncable == 0 {
return httperrors.NewInvalidStatusError("no syncable instances")
}
return nil
}
func (model *SLLMDeployment) PerformSyncstatus(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
input *api.LLMDeploymentSyncstatusInput,
) (jsonutils.JSONObject, error) {
if err := model.ValidateSyncstatusInput(ctx, userCred, input); err != nil {
return nil, err
}
if err := model.StartSyncStatusTask(ctx, userCred); err != nil {
return nil, errors.Wrap(err, "StartSyncStatusTask")
}
return nil, nil
}
func (model *SLLMDeployment) StartSyncStatusTask(ctx context.Context, userCred mcclient.TokenCredential) error {
model.SetStatus(ctx, userCred, api.LLM_DEPLOYMENT_STATUS_SYNCING, "")
task, err := taskman.TaskManager.NewTask(ctx, "LLMDeploymentSyncStatusTask", model, userCred, nil, "", "", nil)
if err != nil {
return errors.Wrap(err, "NewTask LLMDeploymentSyncStatusTask")
}
return task.ScheduleRun(nil)
}
func (model *SLLMDeployment) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
if err := DeleteDeploymentAiproxyResources(ctx, model.Id); err != nil {
log.Warningf("CustomizeDelete: delete aiproxy resources for %s: %v", model.Name, err)
+31 -18
View File
@@ -2,7 +2,6 @@ package models
import (
"context"
"encoding/json"
"math"
"strconv"
"strings"
@@ -44,17 +43,38 @@ func deploymentAutoGpuMemoryUtilizationEnabled(auto *bool, llmType string) bool
return ok
}
func defaultDeploymentAutoGpuMemoryUtilization(input *api.LLMDeploymentCreateInput, llmType string) {
func defaultDeploymentAutoGpuMemoryUtilization(input *api.LLMDeploymentCreateInput, sku *SLLMSku, llmType string) {
if input == nil || input.GpuMemoryUtilization != nil || input.AutoGpuMemoryUtilization != nil {
return
}
if !deploymentAutoGpuMemoryUtilizationEnabled(nil, llmType) {
return
}
if !skuCanAutoGpuMemoryUtilization(sku) {
return
}
enabled := true
input.AutoGpuMemoryUtilization = &enabled
}
// disableDeploymentAutoGpuMemoryUtilizationForLocalPath forces auto GPU memory utilization off for host-mounted SKUs.
func disableDeploymentAutoGpuMemoryUtilizationForLocalPath(input *api.LLMDeploymentCreateInput, sku *SLLMSku) {
if input == nil || sku == nil || !SkuHasLocalHostPathModel(sku) {
return
}
disabled := false
input.AutoGpuMemoryUtilization = &disabled
}
// skuCanAutoGpuMemoryUtilization reports whether auto GPU memory utilization can be derived for sku.
// Host-mounted local_path SKUs always opt out; use gpu_memory_utilization manually when needed.
func skuCanAutoGpuMemoryUtilization(sku *SLLMSku) bool {
if sku == nil {
return true
}
return !SkuHasLocalHostPathModel(sku)
}
func validateDeploymentGpuMemoryUtilization(util *float64, auto *bool, llmType string) error {
needsRuntimeArg := util != nil || boolPtrValue(auto)
if util != nil {
@@ -154,8 +174,7 @@ func runtimeHasExplicitArg(sku *SLLMSku, keys []string) bool {
return false
}
func backendParametersContainRuntimeArg(raw string, keys []string) bool {
items := backendParameterItems(raw)
func backendParametersContainRuntimeArg(items []string, keys []string) bool {
for i := range items {
argKey, _, ok := splitBackendParameterFlag(items[i])
if ok && runtimeArgKeyIn(argKey, keys) {
@@ -165,8 +184,7 @@ func backendParametersContainRuntimeArg(raw string, keys []string) bool {
return false
}
func tokenLimitFromBackendParameters(raw string, keys []string) (int64, bool) {
items := backendParameterItems(raw)
func tokenLimitFromBackendParameters(items []string, keys []string) (int64, bool) {
var tokenLimit int64
found := false
for i := range items {
@@ -188,18 +206,6 @@ func tokenLimitFromBackendParameters(raw string, keys []string) (int64, bool) {
return tokenLimit, found
}
func backendParameterItems(raw string) []string {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
items := []string{}
if err := json.Unmarshal([]byte(raw), &items); err != nil {
return []string{raw}
}
return items
}
func splitBackendParameterFlag(item string) (string, string, bool) {
item = strings.TrimSpace(item)
if item == "" || strings.HasPrefix(item, "-") && !strings.HasPrefix(item, "--") {
@@ -477,6 +483,9 @@ func BuildDeploymentResolvedGpuMemoryLLMSpec(ctx context.Context, userCred mccli
if deploy.GpuMemoryUtilization != nil {
return buildDeploymentGpuMemoryLLMSpec(deploy, sku)
}
if !skuCanAutoGpuMemoryUtilization(sku) {
return nil, nil
}
if !deploymentAutoGpuMemoryUtilizationEnabled(deploy.AutoGpuMemoryUtilization, sku.LLMType) {
return nil, nil
}
@@ -500,6 +509,10 @@ func BuildDeploymentResolvedGpuMemoryLLMSpec(ctx context.Context, userCred mccli
}
func maxMountedModelVramRequirementMB(sku *SLLMSku) (int64, error) {
if sku != nil && SkuHasLocalHostPathModel(sku) {
return 0, httperrors.NewInputParameterError(
"auto_gpu_memory_utilization is not supported for local_path SKU: set gpu_memory_utilization manually or omit both")
}
modelIds := sku.GetMountedModels()
if len(modelIds) == 0 {
return 0, httperrors.NewInputParameterError("auto_gpu_memory_utilization requires mounted models: configure mounted_models on the LLM SKU")
@@ -0,0 +1,66 @@
package models
import (
"testing"
api "yunion.io/x/onecloud/pkg/apis/llm"
)
func localPathTestSku() *SLLMSku {
return &SLLMSku{
LLMType: string(api.LLM_CONTAINER_VLLM),
Source: api.LLM_MODEL_SOURCE_LOCAL_PATH,
LocalPath: "/data/models/Qwen3-8B",
SLLMSkuBase: SLLMSkuBase{
HostPaths: &api.HostPaths{{
Type: "directory",
Path: "/data/models/Qwen3-8B",
Containers: map[string]*api.ContainerHostPathRelation{
"0": {MountPath: "/data/models/huggingface/Qwen3-8B"},
},
}},
},
}
}
func TestSkuCanAutoGpuMemoryUtilizationLocalPath(t *testing.T) {
localSku := localPathTestSku()
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")
}
}
func TestDefaultDeploymentAutoGpuMemoryUtilizationSkipsLocalPath(t *testing.T) {
input := &api.LLMDeploymentCreateInput{}
sku := localPathTestSku()
defaultDeploymentAutoGpuMemoryUtilization(input, sku, string(api.LLM_CONTAINER_VLLM))
if input.AutoGpuMemoryUtilization != nil {
t.Fatalf("expected auto_gpu_memory_utilization unset for local_path SKU, got %v", *input.AutoGpuMemoryUtilization)
}
}
func TestDisableDeploymentAutoGpuMemoryUtilizationForLocalPath(t *testing.T) {
input := &api.LLMDeploymentCreateInput{
AutoGpuMemoryUtilization: func() *bool { v := true; return &v }(),
}
disableDeploymentAutoGpuMemoryUtilizationForLocalPath(input, localPathTestSku())
if input.AutoGpuMemoryUtilization == nil || *input.AutoGpuMemoryUtilization {
t.Fatalf("expected auto_gpu_memory_utilization forced false, got %v", input.AutoGpuMemoryUtilization)
}
}
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,31 @@
package models
import (
"testing"
api "yunion.io/x/onecloud/pkg/apis/llm"
)
func TestCanRestartDeploymentStatus(t *testing.T) {
cases := []struct {
status string
want bool
}{
{api.STATUS_READY, true},
{api.LLM_DEPLOYMENT_STATUS_PARTIAL, true},
{api.LLM_STATUS_RUNNING, true},
{api.LLM_DEPLOYMENT_STATUS_DEPLOYING, false},
{api.LLM_STATUS_DELETING, false},
{api.LLM_DEPLOYMENT_STATUS_IMPORTING_MODEL, false},
{api.LLM_DEPLOYMENT_STATUS_CREATING_SKU, false},
{api.LLM_STATUS_CREATE_FAIL, false},
{api.LLM_STATUS_DELETE_FAILED, false},
{"unknown", false},
}
for _, tc := range cases {
got := canRestartDeploymentStatus(tc.status)
if got != tc.want {
t.Errorf("canRestartDeploymentStatus(%q) = %v, want %v", tc.status, got, tc.want)
}
}
}
+31 -13
View File
@@ -2,6 +2,7 @@ package models
import (
"context"
"fmt"
"strings"
"yunion.io/x/jsonutils"
@@ -58,18 +59,16 @@ type SLLMSku struct {
LLMSpec *api.LLMSpec `json:"llm_spec" length:"long" list:"user" create:"optional" update:"user"`
// Model source
Source string `width:"32" charset:"ascii" nullable:"true" list:"user" create:"optional" update:"user"`
HuggingfaceRepoId string `width:"256" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
HuggingfaceFilename string `width:"256" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
ModelScopeModelId string `width:"256" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
ModelScopeFilePath string `width:"256" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
LocalPath string `width:"512" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
// Model categories, JSON array: ["llm"], ["embedding"], ["image"]
Categories string `charset:"utf8" length:"medium" nullable:"true" list:"user" create:"optional" update:"user"`
// Inference backend version
BackendVersion string `width:"64" charset:"ascii" nullable:"true" list:"user" create:"optional" update:"user"`
// Backend CLI parameters, JSON array
BackendParameters string `charset:"utf8" length:"long" nullable:"true" list:"user" create:"optional" update:"user"`
Source string `width:"32" charset:"ascii" nullable:"true" list:"user" create:"optional" update:"user"`
HuggingfaceRepoId string `width:"256" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
HuggingfaceFilename string `width:"256" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
ModelScopeModelId string `width:"256" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
ModelScopeFilePath string `width:"256" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
LocalPath string `width:"512" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
PreferHosts []string `charset:"utf8" length:"medium" nullable:"true" list:"user" create:"optional" update:"user"`
Categories []string `charset:"utf8" length:"medium" nullable:"true" list:"user" create:"optional" update:"user"`
BackendVersion string `width:"64" charset:"ascii" nullable:"true" list:"user" create:"optional" update:"user"`
BackendParameters []string `charset:"utf8" length:"long" nullable:"true" list:"user" create:"optional" update:"user"`
}
func (man *SLLMSkuManager) ListItemFilter(
@@ -97,7 +96,7 @@ func (man *SLLMSkuManager) ListItemFilter(
q = q.Equals("source", input.Source)
}
if len(input.Categories) > 0 {
q = q.Contains("categories", input.Categories)
q = q.Contains("categories", fmt.Sprintf("%q", input.Categories))
}
return q, nil
}
@@ -147,6 +146,7 @@ func (manager *SLLMSkuManager) FetchCustomizeColumns(
res[i].ModelScopeModelId = sku.ModelScopeModelId
res[i].ModelScopeFilePath = sku.ModelScopeFilePath
res[i].LocalPath = sku.LocalPath
res[i].PreferHosts = GetSkuPreferHosts(&sku)
res[i].Categories = sku.Categories
res[i].BackendVersion = sku.BackendVersion
res[i].BackendParameters = sku.BackendParameters
@@ -239,6 +239,16 @@ func (man *SLLMSkuManager) ValidateCreateData(ctx context.Context, userCred mccl
if err != nil {
return input, errors.Wrap(err, "validate create input")
}
if isLocalPathSkuCreate(input) {
if err := ValidateLocalPathSkuCreate(input); err != nil {
return input, err
}
resolved, err := resolvePreferHosts(ctx, userCred, input.PreferHosts)
if err != nil {
return input, err
}
input.PreferHosts = resolved
}
importInput, err := resolveLLMSkuImport(input)
if err != nil {
return input, err
@@ -251,6 +261,14 @@ func (man *SLLMSkuManager) ValidateCreateData(ctx context.Context, userCred mccl
return input, nil
}
func (sku *SLLMSku) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
err := sku.SSharableVirtualResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
if err != nil {
return errors.Wrap(err, "SSharableVirtualResourceBase.CustomizeCreate")
}
return nil
}
func (sku *SLLMSku) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
sku.SSharableVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
input := api.LLMSkuCreateInput{}
+88
View File
@@ -0,0 +1,88 @@
package models
import (
"fmt"
"strings"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/llm"
"yunion.io/x/onecloud/pkg/httperrors"
)
func isLocalPathSkuCreate(input *api.LLMSkuCreateInput) bool {
if input == nil {
return false
}
return strings.EqualFold(strings.TrimSpace(input.Source), api.LLM_MODEL_SOURCE_LOCAL_PATH)
}
// ValidateLocalPathSkuCreate validates SKU create requests that mount an on-host model directory.
func ValidateLocalPathSkuCreate(input *api.LLMSkuCreateInput) error {
if input == nil {
return errors.Wrap(httperrors.ErrInputParameter, "empty sku input")
}
llmType := strings.TrimSpace(input.LLMType)
if llmType != string(api.LLM_CONTAINER_VLLM) && llmType != string(api.LLM_CONTAINER_SGLANG) {
return errors.Wrapf(httperrors.ErrInputParameter, "local_path import supports vllm and sglang only, got %q", llmType)
}
localPath := strings.TrimSpace(input.LocalPath)
if localPath == "" {
return errors.Wrap(httperrors.ErrMissingParameter, "local_path is required for local_path source")
}
if !strings.HasPrefix(localPath, "/") {
return errors.Wrap(httperrors.ErrInputParameter, "local_path must be an absolute path")
}
if input.ModelSpec != nil {
return errors.Wrap(httperrors.ErrInputParameter, "model_spec is not allowed for local_path import")
}
if input.HostPaths == nil || input.HostPaths.IsZero() {
return errors.Wrap(httperrors.ErrMissingParameter, "host_paths is required for local_path source")
}
if !hostPathsHasContainerMount(*input.HostPaths, 0) {
return errors.Wrap(httperrors.ErrInputParameter, "host_paths must include a mount for container index 0")
}
if len(normalizePreferHostInputs(input.PreferHosts)) == 0 {
return errors.Wrap(httperrors.ErrMissingParameter, "prefer_hosts is required for local_path source")
}
input.Source = api.LLM_MODEL_SOURCE_LOCAL_PATH
input.LocalPath = localPath
return nil
}
func hostPathsHasContainerMount(paths api.HostPaths, containerIndex int) bool {
key := fmt.Sprintf("%d", containerIndex)
for _, hp := range paths {
if hp.IsZero() {
continue
}
if hp.Containers == nil {
continue
}
rel, ok := hp.Containers[key]
if !ok || rel == nil {
continue
}
if strings.TrimSpace(rel.MountPath) != "" {
return true
}
}
return false
}
// SkuHasLocalHostPathModel reports whether sku carries a host-mounted local model (no InstantModel import).
func SkuHasLocalHostPathModel(sku *SLLMSku) bool {
if sku == nil {
return false
}
if strings.TrimSpace(sku.Source) != api.LLM_MODEL_SOURCE_LOCAL_PATH {
return false
}
if strings.TrimSpace(sku.LocalPath) == "" {
return false
}
if sku.HostPaths == nil || sku.HostPaths.IsZero() {
return false
}
return hostPathsHasContainerMount(*sku.HostPaths, 0)
}
+141
View File
@@ -0,0 +1,141 @@
package models
import (
"testing"
"yunion.io/x/onecloud/pkg/apis/llm"
)
func TestValidateLocalPathSkuCreate(t *testing.T) {
hostPaths := llm.HostPaths{
{
Type: "directory",
Path: "/data/models/Qwen3-8B",
Containers: llm.ContainerHostPathRelations{
"0": {MountPath: "/data/models/huggingface/Qwen3-8B", ReadOnly: true},
},
},
}
input := &llm.LLMSkuCreateInput{
LLMSKuBaseCreateInput: llm.LLMSKuBaseCreateInput{
HostPaths: &hostPaths,
},
LLMType: string(llm.LLM_CONTAINER_VLLM),
Source: llm.LLM_MODEL_SOURCE_LOCAL_PATH,
LocalPath: "/data/models/Qwen3-8B",
PreferHosts: []string{"host-1"},
}
if err := ValidateLocalPathSkuCreate(input); err != nil {
t.Fatalf("expected valid local_path sku, got %v", err)
}
if input.Source != llm.LLM_MODEL_SOURCE_LOCAL_PATH {
t.Fatalf("expected source local_path, got %q", input.Source)
}
}
func TestValidateLocalPathSkuCreateRejectsModelSpec(t *testing.T) {
hostPaths := llm.HostPaths{
{
Type: "directory",
Path: "/data/models/Qwen3-8B",
Containers: llm.ContainerHostPathRelations{
"0": {MountPath: "/data/models/huggingface/Qwen3-8B"},
},
},
}
input := &llm.LLMSkuCreateInput{
LLMSKuBaseCreateInput: llm.LLMSKuBaseCreateInput{
HostPaths: &hostPaths,
},
LLMType: string(llm.LLM_CONTAINER_VLLM),
Source: llm.LLM_MODEL_SOURCE_LOCAL_PATH,
LocalPath: "/data/models/Qwen3-8B",
PreferHosts: []string{"host-1"},
ModelSpec: &llm.InstantModelImportInput{ModelName: "x", ModelTag: "main"},
}
if err := ValidateLocalPathSkuCreate(input); err == nil {
t.Fatal("expected error when model_spec is set")
}
}
func TestValidateLocalPathSkuCreateRequiresPreferHosts(t *testing.T) {
hostPaths := llm.HostPaths{
{
Type: "directory",
Path: "/data/models/Qwen3-8B",
Containers: llm.ContainerHostPathRelations{
"0": {MountPath: "/data/models/huggingface/Qwen3-8B"},
},
},
}
input := &llm.LLMSkuCreateInput{
LLMSKuBaseCreateInput: llm.LLMSKuBaseCreateInput{
HostPaths: &hostPaths,
},
LLMType: string(llm.LLM_CONTAINER_VLLM),
Source: llm.LLM_MODEL_SOURCE_LOCAL_PATH,
LocalPath: "/data/models/Qwen3-8B",
}
if err := ValidateLocalPathSkuCreate(input); err == nil {
t.Fatal("expected error when prefer_hosts is missing")
}
}
func TestValidateLocalPathSkuCreateRequiresContainerMount(t *testing.T) {
hostPaths := llm.HostPaths{
{Type: "directory", Path: "/data/models/Qwen3-8B"},
}
input := &llm.LLMSkuCreateInput{
LLMSKuBaseCreateInput: llm.LLMSKuBaseCreateInput{
HostPaths: &hostPaths,
},
LLMType: string(llm.LLM_CONTAINER_SGLANG),
Source: llm.LLM_MODEL_SOURCE_LOCAL_PATH,
LocalPath: "/data/models/Qwen3-8B",
PreferHosts: []string{"host-1"},
}
if err := ValidateLocalPathSkuCreate(input); err == nil {
t.Fatal("expected error when container 0 mount is missing")
}
}
func TestSkuHasLocalHostPathModel(t *testing.T) {
hostPaths := llm.HostPaths{
{
Type: "directory",
Path: "/data/models/Qwen3-8B",
Containers: llm.ContainerHostPathRelations{
"0": {MountPath: "/data/models/huggingface/Qwen3-8B"},
},
},
}
sku := &SLLMSku{
Source: llm.LLM_MODEL_SOURCE_LOCAL_PATH,
LocalPath: "/data/models/Qwen3-8B",
}
sku.HostPaths = &hostPaths
if !SkuHasLocalHostPathModel(sku) {
t.Fatal("expected local host path model sku")
}
}
func TestValidateRequireMountedModelsSkipsLocalPathSku(t *testing.T) {
hostPaths := llm.HostPaths{
{
Type: "directory",
Path: "/data/models/Qwen3-8B",
Containers: llm.ContainerHostPathRelations{
"0": {MountPath: "/data/models/huggingface/Qwen3-8B"},
},
},
}
sku := &SLLMSku{
LLMType: string(llm.LLM_CONTAINER_VLLM),
Source: llm.LLM_MODEL_SOURCE_LOCAL_PATH,
LocalPath: "/data/models/Qwen3-8B",
}
sku.HostPaths = &hostPaths
if err := ValidateRequireMountedModels(string(llm.LLM_CONTAINER_VLLM), nil, nil, sku); err != nil {
t.Fatalf("expected mounted_models not required for local_path sku, got %v", err)
}
}
+9 -6
View File
@@ -33,6 +33,12 @@ func resolveLLMStatusFromPod(currentStatus string, serverStatus string, primaryC
reason := fmt.Sprintf("pod status=%s primary_container_status=%s", serverStatus, primaryContainerStatus)
switch {
case serverStatus == computeapi.VM_READY:
targetStatus = api.LLM_STATUS_READY
case isPrimaryContainerRunning(primaryContainerStatus):
targetStatus = api.LLM_STATUS_RUNNING
case serverStatus == computeapi.VM_RUNNING && primaryContainerStatus == computeapi.CONTAINER_STATUS_PROBING:
targetStatus = api.LLM_STATUS_PROBING
case isLLMPodCrashLoopStatus(serverStatus, primaryContainerStatus) || isLLMStartupProbeFailedStatus(primaryContainerStatus):
if currentStatus == commonapi.STATUS_CREATING {
targetStatus = api.LLM_STATUS_CREATE_FAIL
@@ -41,12 +47,6 @@ func resolveLLMStatusFromPod(currentStatus string, serverStatus string, primaryC
}
case serverStatus == computeapi.POD_STATUS_CONTAINER_EXITED || primaryContainerStatus == computeapi.CONTAINER_STATUS_EXITED:
targetStatus = api.LLM_STATUS_START_FAIL
case serverStatus == computeapi.VM_RUNNING && primaryContainerStatus == computeapi.CONTAINER_STATUS_PROBING:
targetStatus = api.LLM_STATUS_PROBING
case serverStatus == computeapi.VM_RUNNING && isPrimaryContainerRunning(primaryContainerStatus):
targetStatus = api.LLM_STATUS_RUNNING
case serverStatus == computeapi.VM_READY:
targetStatus = api.LLM_STATUS_READY
default:
return llmStatusResolution{}
}
@@ -65,6 +65,9 @@ func resolveLLMStatusFromPod(currentStatus string, serverStatus string, primaryC
}
func isLLMPodCrashLoopStatus(serverStatus string, primaryContainerStatus string) bool {
if isPrimaryContainerRunning(primaryContainerStatus) {
return false
}
return serverStatus == computeapi.POD_STATUS_CRASH_LOOP_BACK_OFF ||
primaryContainerStatus == computeapi.CONTAINER_STATUS_CRASH_LOOP_BACK_OFF
}
+102
View File
@@ -0,0 +1,102 @@
package models
import (
"testing"
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
api "yunion.io/x/onecloud/pkg/apis/llm"
)
func TestResolveLLMStatusFromPod(t *testing.T) {
cases := []struct {
name string
currentStatus string
serverStatus string
primaryContainerStatus string
wantStatus string
wantUpdate bool
}{
{
name: "server ready container exited after start_fail sync",
currentStatus: api.LLM_STATUS_START_FAIL,
serverStatus: computeapi.VM_READY,
primaryContainerStatus: computeapi.CONTAINER_STATUS_EXITED,
wantStatus: api.LLM_STATUS_READY,
wantUpdate: true,
},
{
name: "server ready stale probe_failed after start_fail sync",
currentStatus: api.LLM_STATUS_START_FAIL,
serverStatus: computeapi.VM_READY,
primaryContainerStatus: computeapi.CONTAINER_STATUS_PROBE_FAILED,
wantStatus: api.LLM_STATUS_READY,
wantUpdate: true,
},
{
name: "already ready no update",
currentStatus: api.LLM_STATUS_READY,
serverStatus: computeapi.VM_READY,
primaryContainerStatus: computeapi.CONTAINER_STATUS_EXITED,
wantUpdate: false,
},
{
name: "running with probe failed is start_fail",
currentStatus: api.LLM_STATUS_RUNNING,
serverStatus: computeapi.VM_RUNNING,
primaryContainerStatus: computeapi.CONTAINER_STATUS_PROBE_FAILED,
wantStatus: api.LLM_STATUS_START_FAIL,
wantUpdate: true,
},
{
name: "running container running",
currentStatus: api.LLM_STATUS_PROBING,
serverStatus: computeapi.VM_RUNNING,
primaryContainerStatus: computeapi.CONTAINER_STATUS_RUNNING,
wantStatus: api.LLM_STATUS_RUNNING,
wantUpdate: true,
},
{
name: "running container exited is start_fail",
currentStatus: api.LLM_STATUS_RUNNING,
serverStatus: computeapi.VM_RUNNING,
primaryContainerStatus: computeapi.CONTAINER_STATUS_EXITED,
wantStatus: api.LLM_STATUS_START_FAIL,
wantUpdate: true,
},
{
name: "start_fail recovers when server crash_loop but container running",
currentStatus: api.LLM_STATUS_START_FAIL,
serverStatus: computeapi.POD_STATUS_CRASH_LOOP_BACK_OFF,
primaryContainerStatus: computeapi.CONTAINER_STATUS_RUNNING,
wantStatus: api.LLM_STATUS_RUNNING,
wantUpdate: true,
},
{
name: "start_fail recovers when server and container running",
currentStatus: api.LLM_STATUS_START_FAIL,
serverStatus: computeapi.VM_RUNNING,
primaryContainerStatus: computeapi.CONTAINER_STATUS_RUNNING,
wantStatus: api.LLM_STATUS_RUNNING,
wantUpdate: true,
},
{
name: "running degrades when container crash_loop",
currentStatus: api.LLM_STATUS_RUNNING,
serverStatus: computeapi.VM_RUNNING,
primaryContainerStatus: computeapi.CONTAINER_STATUS_CRASH_LOOP_BACK_OFF,
wantStatus: api.LLM_STATUS_START_FAIL,
wantUpdate: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := resolveLLMStatusFromPod(tc.currentStatus, tc.serverStatus, tc.primaryContainerStatus)
if got.Update != tc.wantUpdate {
t.Fatalf("Update = %v, want %v", got.Update, tc.wantUpdate)
}
if tc.wantUpdate && got.Status != tc.wantStatus {
t.Fatalf("Status = %q, want %q", got.Status, tc.wantStatus)
}
})
}
}
+141
View File
@@ -0,0 +1,141 @@
package models
import (
"context"
"strings"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/sets"
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"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules/compute"
)
// GetSkuPreferHosts returns host ids stored on sku.
func GetSkuPreferHosts(sku *SLLMSku) []string {
if sku == nil || len(sku.PreferHosts) == 0 {
return nil
}
return append([]string(nil), sku.PreferHosts...)
}
// GetDeploymentPreferHosts returns deployment prefer_hosts, falling back to sku.
func GetDeploymentPreferHosts(dep *SLLMDeployment, sku *SLLMSku) []string {
if dep != nil && len(dep.PreferHosts) > 0 {
return append([]string(nil), dep.PreferHosts...)
}
return GetSkuPreferHosts(sku)
}
// SelectPreferHostForInstanceIndex picks a host from prefer_hosts using round-robin.
func SelectPreferHostForInstanceIndex(hosts []string, index int) string {
if len(hosts) == 0 {
return ""
}
if index < 0 {
index = 0
}
return hosts[index%len(hosts)]
}
func validatePreferHostsSubset(selected, allowed []string) error {
if len(selected) == 0 {
return httperrors.NewMissingParameterError("prefer_hosts")
}
if len(allowed) == 0 {
return nil
}
allowedSet := sets.NewString(allowed...)
for _, h := range selected {
if !allowedSet.Has(h) {
return httperrors.NewInputParameterError("prefer_hosts %q is not declared on the LLM SKU", h)
}
}
return nil
}
func normalizePreferHostInputs(hosts []string) []string {
seen := sets.NewString()
out := make([]string, 0, len(hosts))
for _, h := range hosts {
h = strings.TrimSpace(h)
if h == "" || seen.Has(h) {
continue
}
seen.Insert(h)
out = append(out, h)
}
return out
}
func resolvePreferHosts(ctx context.Context, userCred mcclient.TokenCredential, hosts []string) ([]string, error) {
hosts = normalizePreferHostInputs(hosts)
if len(hosts) == 0 {
return nil, httperrors.NewMissingParameterError("prefer_hosts")
}
s := auth.GetSession(ctx, userCred, "")
resolved := make([]string, 0, len(hosts))
for _, hostRef := range hosts {
hostJson, err := compute.Hosts.Get(s, hostRef, nil)
if err != nil {
return nil, errors.Wrapf(err, "get host %s", hostRef)
}
hostDetails := computeapi.HostDetails{}
if err := hostJson.Unmarshal(&hostDetails); err != nil {
return nil, errors.Wrap(err, "unmarshal hostDetails")
}
if hostDetails.Enabled == nil || !*hostDetails.Enabled {
return nil, errors.Wrapf(httperrors.ErrInvalidStatus, "host %s is not enabled", hostRef)
}
if hostDetails.HostStatus != computeapi.HOST_ONLINE {
return nil, errors.Wrapf(httperrors.ErrInvalidStatus, "host %s is not online", hostRef)
}
if hostDetails.HostType != computeapi.HOST_TYPE_CONTAINER {
return nil, errors.Wrapf(httperrors.ErrNotAcceptable, "host %s type %s is not supported", hostRef, hostDetails.HostType)
}
resolved = append(resolved, hostDetails.Id)
}
return normalizePreferHostInputs(resolved), nil
}
func resolvePreferHost(ctx context.Context, userCred mcclient.TokenCredential, hostRef string) (string, error) {
hosts, err := resolvePreferHosts(ctx, userCred, []string{hostRef})
if err != nil {
return "", err
}
if len(hosts) == 0 {
return "", httperrors.NewMissingParameterError("prefer_host")
}
return hosts[0], nil
}
func validateLocalPathDeploymentPreferHosts(
ctx context.Context,
userCred mcclient.TokenCredential,
input *api.LLMDeploymentCreateInput,
sku *SLLMSku,
) error {
if input == nil || sku == nil || !SkuHasLocalHostPathModel(sku) {
return nil
}
skuHosts := GetSkuPreferHosts(sku)
if len(input.PreferHosts) == 0 {
input.PreferHosts = append([]string(nil), skuHosts...)
}
if len(input.PreferHosts) == 0 {
return httperrors.NewMissingParameterError("prefer_hosts is required for local_path SKU")
}
resolved, err := resolvePreferHosts(ctx, userCred, input.PreferHosts)
if err != nil {
return err
}
if err := validatePreferHostsSubset(resolved, skuHosts); err != nil {
return err
}
input.PreferHosts = resolved
return nil
}
+78
View File
@@ -0,0 +1,78 @@
package models
import (
"testing"
)
func TestNormalizePreferHostInputs(t *testing.T) {
got := normalizePreferHostInputs([]string{" host-1 ", "host-2", "host-1", "", "host-3"})
want := []string{"host-1", "host-2", "host-3"}
if len(got) != len(want) {
t.Fatalf("got %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("got %v, want %v", got, want)
}
}
}
func TestGetSkuPreferHosts(t *testing.T) {
sku := &SLLMSku{PreferHosts: []string{"host-a", "host-b"}}
got := GetSkuPreferHosts(sku)
if len(got) != 2 || got[0] != "host-a" || got[1] != "host-b" {
t.Fatalf("unexpected hosts: %v", got)
}
}
func TestGetDeploymentPreferHostsPrefersDeployment(t *testing.T) {
sku := &SLLMSku{PreferHosts: []string{"sku-host"}}
dep := &SLLMDeployment{PreferHosts: []string{"dep-host"}}
got := GetDeploymentPreferHosts(dep, sku)
if len(got) != 1 || got[0] != "dep-host" {
t.Fatalf("expected deployment hosts, got %v", got)
}
}
func TestGetDeploymentPreferHostsFallsBackToSku(t *testing.T) {
sku := &SLLMSku{PreferHosts: []string{"sku-host"}}
dep := &SLLMDeployment{}
got := GetDeploymentPreferHosts(dep, sku)
if len(got) != 1 || got[0] != "sku-host" {
t.Fatalf("expected sku hosts, got %v", got)
}
}
func TestSelectPreferHostForInstanceIndexRoundRobin(t *testing.T) {
hosts := []string{"h1", "h2", "h3"}
cases := []struct {
index int
want string
}{
{0, "h1"},
{1, "h2"},
{2, "h3"},
{3, "h1"},
{5, "h3"},
}
for _, c := range cases {
if got := SelectPreferHostForInstanceIndex(hosts, c.index); got != c.want {
t.Fatalf("index %d: got %q, want %q", c.index, got, c.want)
}
}
if got := SelectPreferHostForInstanceIndex(nil, 0); got != "" {
t.Fatalf("expected empty for nil hosts, got %q", got)
}
}
func TestValidatePreferHostsSubset(t *testing.T) {
if err := validatePreferHostsSubset([]string{"h1"}, []string{"h1", "h2"}); err != nil {
t.Fatalf("expected valid subset, got %v", err)
}
if err := validatePreferHostsSubset([]string{"h3"}, []string{"h1", "h2"}); err == nil {
t.Fatal("expected error for host not on sku")
}
if err := validatePreferHostsSubset(nil, []string{"h1"}); err == nil {
t.Fatal("expected error for empty selected hosts")
}
}
+40 -13
View File
@@ -10,6 +10,7 @@ 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/util/logclient"
)
type LLMAiproxySyncTask struct {
@@ -20,11 +21,45 @@ func init() {
taskman.RegisterTask(LLMAiproxySyncTask{})
}
func (task *LLMAiproxySyncTask) aiproxyLogAction() string {
unregister, _ := task.GetParams().Bool("unregister")
if unregister {
return logclient.ACT_UNREGISTER_AIPROXY
}
return logclient.ACT_REGISTER_AIPROXY
}
func (task *LLMAiproxySyncTask) logAiproxySync(ctx context.Context, dep *models.SLLMDeployment, err error) {
action := task.aiproxyLogAction()
if err != nil {
db.OpsLog.LogEvent(dep, "aiproxy_sync", err.Error(), task.UserCred)
logclient.AddActionLogWithStartable(task, dep, action, err, task.UserCred, false)
return
}
depObj, fetchErr := models.GetLLMDeploymentManager().FetchById(dep.Id)
if fetchErr == nil {
dep = depObj.(*models.SLLMDeployment)
}
if dep.AiproxySyncStatus == api.AIPROXY_SYNC_STATUS_FAILED {
msg := models.AiproxySyncFailureReason(dep)
if msg == "" {
msg = "aiproxy sync failed"
}
db.OpsLog.LogEvent(dep, "aiproxy_sync", msg, task.UserCred)
logclient.AddActionLogWithStartable(task, dep, action, msg, task.UserCred, false)
return
}
logclient.AddActionLogWithStartable(task, dep, action, nil, task.UserCred, true)
}
func (task *LLMAiproxySyncTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
dep := obj.(*models.SLLMDeployment)
if !dep.AutoRegisterAiproxy {
task.SetStageComplete(ctx, nil)
return
unregister, _ := task.GetParams().Bool("unregister")
if !unregister {
task.SetStageComplete(ctx, nil)
return
}
}
llmId, _ := task.GetParams().GetString("llm_id")
@@ -51,19 +86,11 @@ func (task *LLMAiproxySyncTask) OnInit(ctx context.Context, obj db.IStandaloneMo
if err != nil {
log.Errorf("LLMAiproxySyncTask deployment=%s: %v", dep.Name, err)
if !isDeploymentHealthyAfterAiproxySync(dep.Status) {
_ = dep.SetStatus(ctx, task.UserCred, api.LLM_DEPLOYMENT_STATUS_AIPROXY_SYNC_FAILED, err.Error())
}
_ = dep.SetAiproxySyncStatus(ctx, task.UserCred, api.AIPROXY_SYNC_STATUS_FAILED, err.Error())
task.logAiproxySync(ctx, dep, err)
task.SetStageFailed(ctx, jsonutils.NewString(err.Error()))
return
}
task.logAiproxySync(ctx, dep, nil)
task.SetStageComplete(ctx, nil)
}
func isDeploymentHealthyAfterAiproxySync(status string) bool {
switch status {
case api.STATUS_READY, api.LLM_DEPLOYMENT_STATUS_PARTIAL:
return true
}
return false
}
@@ -373,6 +373,7 @@ func scaleUp(ctx context.Context, userCred mcclient.TokenCredential, deployment
if err != nil {
return fmt.Errorf("resolve GPU memory utilization: %w", err)
}
preferHosts := models.GetDeploymentPreferHosts(deployment, llmSku)
var lastErr error
created := 0
@@ -380,7 +381,8 @@ func scaleUp(ctx context.Context, userCred mcclient.TokenCredential, deployment
instanceName := fmt.Sprintf("%s-%d", deployment.Name, nextIndex+i)
// Build typed LLMCreateInput, then marshal to JSON for handler.Create
llmInput := buildDeploymentLLMCreateInput(deployment, nets, imageId, llmSpec)
preferHost := models.SelectPreferHostForInstanceIndex(preferHosts, nextIndex+i)
llmInput := buildDeploymentLLMCreateInput(deployment, nets, imageId, llmSpec, preferHost)
llmParams := jsonutils.Marshal(llmInput).(*jsonutils.JSONDict)
// Name lives on the embedded VirtualResourceCreateInput → set explicitly
@@ -405,11 +407,12 @@ func scaleUp(ctx context.Context, userCred mcclient.TokenCredential, deployment
return nil
}
func buildDeploymentLLMCreateInput(deployment *models.SLLMDeployment, nets []*computeapi.NetworkConfig, imageId string, llmSpec *api.LLMSpec) api.LLMCreateInput {
func buildDeploymentLLMCreateInput(deployment *models.SLLMDeployment, nets []*computeapi.NetworkConfig, imageId string, llmSpec *api.LLMSpec, preferHost string) api.LLMCreateInput {
return api.LLMCreateInput{
LLMBaseCreateInput: api.LLMBaseCreateInput{
AutoStart: deployment.AutoStart,
Nets: nets,
AutoStart: deployment.AutoStart,
Nets: nets,
PreferHost: preferHost,
},
LLMSkuId: deployment.LLMSkuId,
LLMImageId: imageId,
@@ -0,0 +1,98 @@
package llm
import (
"context"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/llm"
"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/util/logclient"
)
// LLMDeploymentRestartTask restarts all restartable SLLM instances under a
// deployment in parallel, then reconciles replica health on the deployment row.
//
// Flow:
// 1. OnInit: enumerate instances, start each restartable instance's
// LLMRestartTask with this task as parent.
// 2. Framework invokes OnInstancesRestarted after all child tasks complete.
// 3. OnInstancesRestarted calls SyncReadyReplicas and completes.
type LLMDeploymentRestartTask struct {
taskman.STask
}
func init() {
taskman.RegisterTask(LLMDeploymentRestartTask{})
}
func (task *LLMDeploymentRestartTask) taskFailed(ctx context.Context, model *models.SLLMDeployment, err error) {
db.OpsLog.LogEvent(model, "restart", err, task.UserCred)
logclient.AddActionLogWithStartable(task, model, logclient.ACT_VM_RESTART, err, task.UserCred, false)
task.SetStageFailed(ctx, jsonutils.NewString(err.Error()))
}
func (task *LLMDeploymentRestartTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
model := obj.(*models.SLLMDeployment)
instances, err := fetchModelInstances(model.Id)
if err != nil {
task.taskFailed(ctx, model, err)
return
}
if len(instances) == 0 {
task.taskFailed(ctx, model, errors.Error("no instances under deployment"))
return
}
task.SetStage("OnInstancesRestarted", nil)
startedCount := 0
for _, inst := range instances {
llmObj, err := models.GetLLMManager().FetchById(inst.Id)
if err != nil {
log.Errorf("LLMDeploymentRestartTask: fetch instance %s: %s", inst.Id, err)
continue
}
llm := llmObj.(*models.SLLM)
taskInput, err := llm.ValidateRestartInput(ctx, task.UserCred, &api.LLMRestartInput{})
if err != nil {
log.Warningf("LLMDeploymentRestartTask: skip instance %s: %s", inst.Id, err)
continue
}
if _, err := llm.StartRestartTask(ctx, task.UserCred, taskInput, task.GetTaskId()); err != nil {
log.Errorf("LLMDeploymentRestartTask: start restart for %s: %s", inst.Id, err)
continue
}
startedCount++
}
if startedCount == 0 {
task.taskFailed(ctx, model, errors.Error("no instance restart tasks could be started"))
}
}
func (task *LLMDeploymentRestartTask) OnInstancesRestarted(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
model := obj.(*models.SLLMDeployment)
if err := model.SyncReadyReplicas(ctx, task.UserCred); err != nil {
log.Warningf("LLMDeploymentRestartTask: SyncReadyReplicas for %s: %s", model.Name, err)
}
db.OpsLog.LogEvent(model, "restart", nil, task.UserCred)
logclient.AddActionLogWithStartable(task, model, logclient.ACT_VM_RESTART, nil, task.UserCred, true)
task.SetStageComplete(ctx, nil)
}
func (task *LLMDeploymentRestartTask) OnInstancesRestartedFailed(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
model := obj.(*models.SLLMDeployment)
log.Warningf("LLMDeploymentRestartTask: some instances failed to restart: %s", body)
if err := model.SyncReadyReplicas(ctx, task.UserCred); err != nil {
log.Warningf("LLMDeploymentRestartTask: SyncReadyReplicas for %s: %s", model.Name, err)
}
db.OpsLog.LogEvent(model, "restart", body, task.UserCred)
logclient.AddActionLogWithStartable(task, model, logclient.ACT_VM_RESTART, body, task.UserCred, false)
task.SetStageComplete(ctx, nil)
}
@@ -0,0 +1,96 @@
package llm
import (
"context"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"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/util/logclient"
)
// LLMDeploymentSyncStatusTask syncs all SLLM instances under a deployment in
// parallel, then reconciles replica health on the deployment row.
//
// Flow:
// 1. OnInit: enumerate instances, start each syncable instance's
// LLMSyncStatusTask with this task as parent.
// 2. Framework invokes OnInstancesSyncStatusComplete after all child tasks complete.
// 3. OnInstancesSyncStatusComplete calls SyncReadyReplicas and completes.
type LLMDeploymentSyncStatusTask struct {
taskman.STask
}
func init() {
taskman.RegisterTask(LLMDeploymentSyncStatusTask{})
}
func (task *LLMDeploymentSyncStatusTask) taskFailed(ctx context.Context, model *models.SLLMDeployment, err error) {
db.OpsLog.LogEvent(model, db.ACT_SYNC_STATUS, err, task.UserCred)
logclient.AddActionLogWithStartable(task, model, logclient.ACT_SYNC_STATUS, err, task.UserCred, false)
task.SetStageFailed(ctx, jsonutils.NewString(err.Error()))
}
func (task *LLMDeploymentSyncStatusTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
model := obj.(*models.SLLMDeployment)
instances, err := fetchModelInstances(model.Id)
if err != nil {
task.taskFailed(ctx, model, err)
return
}
if len(instances) == 0 {
task.taskFailed(ctx, model, errors.Error("no instances under deployment"))
return
}
task.SetStage("OnInstancesSyncStatusComplete", nil)
startedCount := 0
for _, inst := range instances {
llmObj, err := models.GetLLMManager().FetchById(inst.Id)
if err != nil {
log.Errorf("LLMDeploymentSyncStatusTask: fetch instance %s: %s", inst.Id, err)
continue
}
llm := llmObj.(*models.SLLM)
if llm.CmpId == "" {
log.Warningf("LLMDeploymentSyncStatusTask: skip instance %s: no cmp_id", inst.Id)
continue
}
if err := llm.StartSyncStatusTask(ctx, task.UserCred, task.GetTaskId()); err != nil {
log.Errorf("LLMDeploymentSyncStatusTask: start syncstatus for %s: %s", inst.Id, err)
continue
}
startedCount++
}
if startedCount == 0 {
task.taskFailed(ctx, model, errors.Error("no instance syncstatus tasks could be started"))
}
}
func (task *LLMDeploymentSyncStatusTask) OnInstancesSyncStatusComplete(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
model := obj.(*models.SLLMDeployment)
if err := model.SyncReadyReplicas(ctx, task.UserCred); err != nil {
log.Warningf("LLMDeploymentSyncStatusTask: SyncReadyReplicas for %s: %s", model.Name, err)
}
db.OpsLog.LogEvent(model, db.ACT_SYNC_STATUS, nil, task.UserCred)
logclient.AddActionLogWithStartable(task, model, logclient.ACT_SYNC_STATUS, nil, task.UserCred, true)
task.SetStageComplete(ctx, nil)
}
func (task *LLMDeploymentSyncStatusTask) OnInstancesSyncStatusCompleteFailed(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
model := obj.(*models.SLLMDeployment)
log.Warningf("LLMDeploymentSyncStatusTask: some instances failed to sync status: %s", body)
if err := model.SyncReadyReplicas(ctx, task.UserCred); err != nil {
log.Warningf("LLMDeploymentSyncStatusTask: SyncReadyReplicas for %s: %s", model.Name, err)
}
db.OpsLog.LogEvent(model, db.ACT_SYNC_STATUS, body, task.UserCred)
logclient.AddActionLogWithStartable(task, model, logclient.ACT_SYNC_STATUS, body, task.UserCred, false)
task.SetStageComplete(ctx, nil)
}
+11 -4
View File
@@ -32,8 +32,15 @@ func (task *LLMSyncStatusTask) setLLMStatus(ctx context.Context, llm *models.SLL
}
}
// applyResolvedLLMStatus persists the reconciled LLM status after sync completes.
// Unlike setLLMStatus, this always writes even when invoked as a child task (e.g.
// LLMDeploymentSyncStatusTask) so derived statuses such as ready are not dropped.
func (task *LLMSyncStatusTask) applyResolvedLLMStatus(ctx context.Context, llm *models.SLLM, status string, reason string) {
llm.SetStatus(ctx, task.UserCred, status, reason)
}
func (task *LLMSyncStatusTask) taskFailed(ctx context.Context, llm *models.SLLM, err string) {
task.setLLMStatus(ctx, llm, computeapi.VM_SYNC_FAIL, err)
task.applyResolvedLLMStatus(ctx, llm, computeapi.VM_SYNC_FAIL, err)
db.OpsLog.LogEvent(llm, db.ACT_SYNC_STATUS, err, task.UserCred)
logclient.AddActionLogWithStartable(task, llm, logclient.ACT_SYNC_STATUS, err, task.UserCred, false)
// llm.NotifyRequest(ctx, task.GetUserCred(), notify.ActionStart, nil, false)
@@ -107,14 +114,14 @@ func (task *LLMSyncStatusTask) OnInit(ctx context.Context, obj db.IStandaloneMod
if err != nil {
return nil, errors.Wrap(err, "WaitServerStatus")
}
task.setLLMStatus(ctx, llm, srv.Status, "stop server")
task.applyResolvedLLMStatus(ctx, llm, srv.Status, "stop server")
} else {
resolved, err := models.ResolveLLMStatusFromServerDetails(ctx, llm, srv)
if err != nil {
return nil, errors.Wrap(err, "ResolveLLMStatusFromServerDetails")
}
if resolved.Update {
task.setLLMStatus(ctx, llm, resolved.Status, resolved.Reason)
task.applyResolvedLLMStatus(ctx, llm, resolved.Status, resolved.Reason)
}
}
@@ -122,7 +129,7 @@ func (task *LLMSyncStatusTask) OnInit(ctx context.Context, obj db.IStandaloneMod
if volume != nil {
disk, err := volume.GetDisk(ctx)
if err != nil {
task.setLLMStatus(ctx, llm, computeapi.VM_DISK_RESET_FAIL, errors.Wrap(err, "GetDisk").Error())
task.applyResolvedLLMStatus(ctx, llm, computeapi.VM_DISK_RESET_FAIL, errors.Wrap(err, "GetDisk").Error())
return nil, errors.Wrap(err, "GetDisk")
}
if disk.Status != computeapi.DISK_READY {
@@ -290,6 +290,30 @@ func (o *LLMDeploymentUnregisterAiproxyOptions) Params() (jsonutils.JSONObject,
return jsonutils.NewDict(), nil
}
type LLMDeploymentRestartOptions struct {
options.BaseIdOptions
}
func (o *LLMDeploymentRestartOptions) GetId() string {
return o.ID
}
func (o *LLMDeploymentRestartOptions) Params() (jsonutils.JSONObject, error) {
return jsonutils.NewDict(), nil
}
type LLMDeploymentSyncstatusOptions struct {
options.BaseIdOptions
}
func (o *LLMDeploymentSyncstatusOptions) GetId() string {
return o.ID
}
func (o *LLMDeploymentSyncstatusOptions) Params() (jsonutils.JSONObject, error) {
return jsonutils.NewDict(), nil
}
func applyGpuUtilizationAlias(params *jsonutils.JSONDict, gpuMemoryUtilization, gpuUtilization *float64) error {
if gpuMemoryUtilization != nil && gpuUtilization != nil {
return fmt.Errorf("--gpu-memory-utilization and --gpu-utilization are aliases; specify only one")
+3
View File
@@ -318,4 +318,7 @@ const (
ACT_CLONE = "clone"
ACT_REBUILD = "rebuild"
ACT_REGISTER_AIPROXY = "register_aiproxy"
ACT_UNREGISTER_AIPROXY = "unregister_aiproxy"
)