feat(llm): add hostpaths for llm-sku (#24693)

This commit is contained in:
cwz_eikoh
2026-04-20 10:51:16 +08:00
committed by GitHub
parent fd157b1be4
commit ff89a3839b
6 changed files with 331 additions and 6 deletions

77
pkg/apis/llm/host_path.go Normal file
View File

@@ -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
}

View File

@@ -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"`

View File

@@ -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

View File

@@ -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
}

View File

@@ -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"`

View File

@@ -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=<host_path>,type=<directory|file>,container_index=<index>,mount_path=<container_path>[,auto_create=<bool>][,read_only=<bool>][,propagation=<private|rslave|rshared>][,fs_user=<uid>][,fs_group=<gid>][,uid=<uid>][,gid=<gid>][,permissions=<mode>]; repeatable"`
Env []string `help:"env in format of key=value"`
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=<host_path>,type=<directory|file>,container_index=<index>,mount_path=<container_path>[,auto_create=<bool>][,read_only=<bool>][,propagation=<private|rslave|rshared>][,fs_user=<uid>][,fs_group=<gid>][,uid=<uid>][,gid=<gid>][,permissions=<mode>]; repeatable"`
Env []string `help:"env in the format of key=value, e.g. AUTHENTICATION_PATH=/bupt-test/"`
Property []string `help:"extra properties of key=value, e.g. tango32=true"`
@@ -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 {