diff --git a/pkg/apis/llm/host_path.go b/pkg/apis/llm/host_path.go new file mode 100644 index 0000000000..c0b1a22252 --- /dev/null +++ b/pkg/apis/llm/host_path.go @@ -0,0 +1,77 @@ +package llm + +import ( + "fmt" + "reflect" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/gotypes" + + "yunion.io/x/onecloud/pkg/apis" +) + +func init() { + gotypes.RegisterSerializable(reflect.TypeOf(&HostPath{}), func() gotypes.ISerializable { + return &HostPath{} + }) + gotypes.RegisterSerializable(reflect.TypeOf(&HostPaths{}), func() gotypes.ISerializable { + return &HostPaths{} + }) + gotypes.RegisterSerializable(reflect.TypeOf(&ContainerHostPathRelations{}), func() gotypes.ISerializable { + return &ContainerHostPathRelations{} + }) +} + +type ContainerHostPathRelation struct { + MountPath string `json:"mount_path"` + ReadOnly bool `json:"read_only"` + Propagation apis.ContainerMountPropagation `json:"propagation,omitempty"` + FsUser *int64 `json:"fs_user"` + FsGroup *int64 `json:"fs_group"` +} + +// key is string format of integer +type ContainerHostPathRelations map[string]*ContainerHostPathRelation + +func (s ContainerHostPathRelations) String() string { + return jsonutils.Marshal(s).String() +} + +func (s ContainerHostPathRelations) IsZero() bool { + return len(s) == 0 +} + +type HostPath struct { + Type apis.ContainerVolumeMountHostPathType `json:"type"` + Path string `json:"path"` + AutoCreate bool `json:"auto_create"` + AutoCreateConfig *apis.ContainerVolumeMountHostPathAutoCreateConfig `json:"auto_create_config,omitempty"` + // Container index to mount path relation + Containers ContainerHostPathRelations `json:"containers"` +} + +func (s HostPath) String() string { + return jsonutils.Marshal(s).String() +} + +func (s HostPath) IsZero() bool { + return len(s.Path) == 0 +} + +func (s HostPath) GetHostPathByContainer(containerIndex int) *ContainerHostPathRelation { + if len(s.Containers) == 0 { + return nil + } + key := fmt.Sprintf("%d", containerIndex) + return s.Containers[key] +} + +type HostPaths []HostPath + +func (s HostPaths) String() string { + return jsonutils.Marshal(s).String() +} + +func (s HostPaths) IsZero() bool { + return len(s) == 0 +} diff --git a/pkg/apis/llm/sku.go b/pkg/apis/llm/sku.go index 4e32e0f87a..ec5296a47d 100644 --- a/pkg/apis/llm/sku.go +++ b/pkg/apis/llm/sku.go @@ -138,6 +138,7 @@ type LLMSKuBaseCreateInput struct { Bandwidth int `json:"bandwidth"` Volumes *Volumes `json:"volumes"` + HostPaths *HostPaths `json:"host_paths"` PortMappings *PortMappings `json:"port_mappings"` Devices *Devices `json:"devices"` Envs *Envs `json:"envs"` @@ -152,11 +153,12 @@ type LLMSkuBaseUpdateInput struct { // RequstSyncImage *bool `json:"request_sync_image"` - DiskSize *int `json:"disk_size" yunion-deprecated-by:"disk_size_mb"` - DiskSizeMB *int `json:"disk_size_mb"` - TemplateId *string `json:"template_id"` - StorageType *string `json:"storage_type"` - Volumes *Volumes `json:"volumes"` + DiskSize *int `json:"disk_size" yunion-deprecated-by:"disk_size_mb"` + DiskSizeMB *int `json:"disk_size_mb"` + TemplateId *string `json:"template_id"` + StorageType *string `json:"storage_type"` + Volumes *Volumes `json:"volumes"` + HostPaths *HostPaths `json:"host_paths"` Bandwidth *int `json:"bandwidth"` PortMappings *PortMappings `json:"port_mappings"` diff --git a/pkg/llm/models/llm_base.go b/pkg/llm/models/llm_base.go index 7d864d90d9..59c768cf1b 100644 --- a/pkg/llm/models/llm_base.go +++ b/pkg/llm/models/llm_base.go @@ -345,6 +345,48 @@ func GetDiskVolumeMounts(vols *api.Volumes, containerIndex int, postOverlays []* return mounts } +func GetHostPathVolumeMounts(hostPaths *api.HostPaths, containerIndex int) []*apis.ContainerVolumeMount { + if hostPaths == nil { + return nil + } + mounts := make([]*apis.ContainerVolumeMount, 0) + for idx, hostPath := range *hostPaths { + hostPathRelation := hostPath.GetHostPathByContainer(containerIndex) + if hostPathRelation == nil { + continue + } + mounts = append(mounts, &apis.ContainerVolumeMount{ + UniqueName: fmt.Sprintf("host-path-%d-%d-%s", idx, containerIndex, hostPathRelation.MountPath), + Type: apis.CONTAINER_VOLUME_MOUNT_TYPE_HOST_PATH, + MountPath: hostPathRelation.MountPath, + ReadOnly: hostPathRelation.ReadOnly, + Propagation: hostPathRelation.Propagation, + HostPath: &apis.ContainerVolumeMountHostPath{ + Type: hostPath.Type, + Path: hostPath.Path, + AutoCreate: hostPath.AutoCreate, + AutoCreateConfig: hostPath.AutoCreateConfig, + }, + FsUser: hostPathRelation.FsUser, + FsGroup: hostPathRelation.FsGroup, + }) + } + return mounts +} + +func AppendLLMSkuVolumeMounts(containers []*computeapi.PodContainerCreateInput, skuBase *SLLMSkuBase, postOverlays []*apis.ContainerVolumeMountDiskPostOverlay) { + if skuBase == nil { + return + } + for idx := range containers { + if containers[idx] == nil { + continue + } + containers[idx].VolumeMounts = append(containers[idx].VolumeMounts, GetDiskVolumeMounts(skuBase.Volumes, idx, postOverlays)...) + containers[idx].VolumeMounts = append(containers[idx].VolumeMounts, GetHostPathVolumeMounts(skuBase.HostPaths, idx)...) + } +} + // 取消自动删除 func (llm *SLLMBase) Delete(ctx context.Context, userCred mcclient.TokenCredential) error { return nil diff --git a/pkg/llm/models/llm_container_driver.go b/pkg/llm/models/llm_container_driver.go index 452220c790..bb374a8060 100644 --- a/pkg/llm/models/llm_container_driver.go +++ b/pkg/llm/models/llm_container_driver.go @@ -150,5 +150,9 @@ func GetLLMContainerInstantModelDriver(typ llm.LLMContainerType) (ILLMContainerI // GetDriverPodContainers returns the container(s) for the given driver. If the driver implements ILLMContainerDriverMultiContainer, GetContainerSpecs is used; otherwise a single-element slice from GetContainerSpec is returned. func GetDriverPodContainers(ctx context.Context, drv ILLMContainerDriver, llm *SLLM, image *SLLMImage, sku *SLLMSku, props []string, devices []computeapi.SIsolatedDevice, diskId string) []*computeapi.PodContainerCreateInput { - return drv.GetContainerSpecs(ctx, llm, image, sku, props, devices, diskId) + containers := drv.GetContainerSpecs(ctx, llm, image, sku, props, devices, diskId) + if sku != nil { + AppendLLMSkuVolumeMounts(containers, &sku.SLLMSkuBase, nil) + } + return containers } diff --git a/pkg/llm/models/sku.go b/pkg/llm/models/sku.go index a7da291d88..3c041bac3d 100644 --- a/pkg/llm/models/sku.go +++ b/pkg/llm/models/sku.go @@ -36,6 +36,7 @@ type SLLMSkuBase struct { 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"` Devices *api.Devices `charset:"utf8" length:"medium" nullable:"true" list:"user" update:"user" create:"optional"` Envs *api.Envs `charset:"utf8" nullable:"true" list:"user" update:"user" create:"optional"` diff --git a/pkg/mcclient/options/llm/llm_sku_base.go b/pkg/mcclient/options/llm/llm_sku_base.go index ef57e3f1e2..7a05967725 100644 --- a/pkg/mcclient/options/llm/llm_sku_base.go +++ b/pkg/mcclient/options/llm/llm_sku_base.go @@ -25,6 +25,7 @@ type LLMSkuBaseCreateOptions struct { 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'"` + HostPaths []string `json:"-" help:"host path mount in format path=,type=,container_index=,mount_path=[,auto_create=][,read_only=][,propagation=][,fs_user=][,fs_group=][,uid=][,gid=][,permissions=]; repeatable"` Env []string `help:"env in format of key=value"` Property []string `help:"extra properties of key=value, e.g. tango32=true"` @@ -44,6 +45,9 @@ func (o *LLMSkuBaseCreateOptions) Params(dict *jsonutils.JSONDict) error { dict.Set("volumes", jsonutils.Marshal(vols)) fetchPortmappings(o.PortMappings, dict) fetchDevices(o.Devices, dict) + if err := fetchHostPaths(o.HostPaths, dict); err != nil { + return err + } fetchEnvs(o.Env, dict) fetchProperties(o.Property, dict) @@ -66,6 +70,7 @@ type LLMSkuBaseUpdateOptions struct { // 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"` + HostPaths []string `json:"-" help:"host path mount in format path=,type=,container_index=,mount_path=[,auto_create=][,read_only=][,propagation=][,fs_user=][,fs_group=][,uid=][,gid=][,permissions=]; 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"` @@ -92,12 +97,206 @@ func (o *LLMSkuBaseUpdateOptions) Params(dict *jsonutils.JSONDict) error { fetchPortmappings(o.PortMappings, dict) fetchDevices(o.Devices, dict) + if err := fetchHostPaths(o.HostPaths, dict); err != nil { + return err + } fetchEnvs(o.Env, dict) fetchProperties(o.Property, dict) // fetchMountedApps(o.MountedApps, dict) return nil } +type hostPathCliSpec struct { + Path string + Type apis.ContainerVolumeMountHostPathType + ContainerIndex int + MountPath string + ReadOnly bool + Propagation apis.ContainerMountPropagation + FsUser *int64 + FsGroup *int64 + AutoCreate bool + AutoCreateConfig *apis.ContainerVolumeMountHostPathAutoCreateConfig +} + +func fetchHostPaths(hostPathStrs []string, dict *jsonutils.JSONDict) error { + hostPaths, err := parseHostPaths(hostPathStrs) + if err != nil { + return err + } + if len(hostPaths) > 0 { + dict.Set("host_paths", jsonutils.Marshal(hostPaths)) + } + return nil +} + +func parseHostPaths(hostPathStrs []string) (api.HostPaths, error) { + if len(hostPathStrs) == 0 { + return nil, nil + } + ret := make(api.HostPaths, 0) + groupIndex := make(map[string]int) + for _, item := range hostPathStrs { + spec, err := parseHostPath(item) + if err != nil { + return nil, err + } + key := getHostPathGroupKey(spec) + idx, ok := groupIndex[key] + if !ok { + ret = append(ret, api.HostPath{ + Type: spec.Type, + Path: spec.Path, + AutoCreate: spec.AutoCreate, + AutoCreateConfig: spec.AutoCreateConfig, + Containers: make(api.ContainerHostPathRelations), + }) + idx = len(ret) - 1 + groupIndex[key] = idx + } + containerKey := strconv.Itoa(spec.ContainerIndex) + if _, exists := ret[idx].Containers[containerKey]; exists { + return nil, fmt.Errorf("duplicate host_path container_index %d for path %q", spec.ContainerIndex, spec.Path) + } + ret[idx].Containers[containerKey] = &api.ContainerHostPathRelation{ + MountPath: spec.MountPath, + ReadOnly: spec.ReadOnly, + Propagation: spec.Propagation, + FsUser: spec.FsUser, + FsGroup: spec.FsGroup, + } + } + return ret, nil +} + +func parseHostPath(item string) (*hostPathCliSpec, error) { + parts := strings.Split(item, ",") + fields := make(map[string]string, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + idx := strings.Index(part, "=") + if idx <= 0 { + return nil, fmt.Errorf("invalid host path %q, expected key=value", item) + } + key := strings.TrimSpace(part[:idx]) + val := strings.TrimSpace(part[idx+1:]) + if key == "" { + return nil, fmt.Errorf("invalid host path %q, empty key", item) + } + fields[key] = val + } + + spec := &hostPathCliSpec{ + Type: apis.CONTAINER_VOLUME_MOUNT_HOST_PATH_TYPE_DIRECTORY, + } + for key, val := range fields { + switch key { + case "path": + spec.Path = val + case "type": + spec.Type = apis.ContainerVolumeMountHostPathType(val) + case "container_index": + idx, err := strconv.Atoi(val) + if err != nil { + return nil, fmt.Errorf("invalid host path container_index %q: %w", val, err) + } + spec.ContainerIndex = idx + case "mount_path": + spec.MountPath = val + case "read_only": + b, err := strconv.ParseBool(val) + if err != nil { + return nil, fmt.Errorf("invalid host path read_only %q: %w", val, err) + } + spec.ReadOnly = b + case "auto_create": + b, err := strconv.ParseBool(val) + if err != nil { + return nil, fmt.Errorf("invalid host path auto_create %q: %w", val, err) + } + spec.AutoCreate = b + case "propagation": + if !apis.ContainerMountPropagations.Has(val) { + return nil, fmt.Errorf("invalid host path propagation %q", val) + } + spec.Propagation = apis.ContainerMountPropagation(val) + case "fs_user": + fsUser, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid host path fs_user %q: %w", val, err) + } + spec.FsUser = &fsUser + case "fs_group": + fsGroup, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid host path fs_group %q: %w", val, err) + } + spec.FsGroup = &fsGroup + case "uid", "gid", "permissions": + if spec.AutoCreateConfig == nil { + spec.AutoCreateConfig = &apis.ContainerVolumeMountHostPathAutoCreateConfig{} + } + spec.AutoCreate = true + switch key { + case "uid": + uid, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid host path uid %q: %w", val, err) + } + spec.AutoCreateConfig.Uid = uint(uid) + case "gid": + gid, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid host path gid %q: %w", val, err) + } + spec.AutoCreateConfig.Gid = uint(gid) + case "permissions": + spec.AutoCreateConfig.Permissions = val + } + default: + return nil, fmt.Errorf("invalid host path key %q", key) + } + } + + if spec.Path == "" { + return nil, fmt.Errorf("invalid host path %q, missing path", item) + } + if spec.MountPath == "" { + return nil, fmt.Errorf("invalid host path %q, missing mount_path", item) + } + if !isValidHostPathType(spec.Type) { + return nil, fmt.Errorf("invalid host path type %q", spec.Type) + } + if _, ok := fields["container_index"]; !ok { + return nil, fmt.Errorf("invalid host path %q, missing container_index", item) + } + return spec, nil +} + +func getHostPathGroupKey(spec *hostPathCliSpec) string { + uid := uint(0) + gid := uint(0) + permissions := "" + if spec.AutoCreateConfig != nil { + uid = spec.AutoCreateConfig.Uid + gid = spec.AutoCreateConfig.Gid + permissions = spec.AutoCreateConfig.Permissions + } + return fmt.Sprintf("%s|%s|%t|%d|%d|%s", spec.Path, spec.Type, spec.AutoCreate, uid, gid, permissions) +} + +func isValidHostPathType(hostPathType apis.ContainerVolumeMountHostPathType) bool { + switch hostPathType { + case apis.CONTAINER_VOLUME_MOUNT_HOST_PATH_TYPE_DIRECTORY, apis.CONTAINER_VOLUME_MOUNT_HOST_PATH_TYPE_FILE: + return true + default: + return false + } +} + func fetchPortmappings(pmStrs []string, dict *jsonutils.JSONDict) { pms := make([]api.PortMapping, 0) for _, pm := range pmStrs {