feat(llm,host): support Hygon DCU runtime for vLLM containers (#25258)

Wire vendor on LLM SKU devices, resolve supplemental groups for DCU
access, source DTK env in vLLM entrypoint, and add hysmi telegraf metrics.
This commit is contained in:
Zexi Li
2026-08-02 13:51:44 +08:00
committed by GitHub
parent 89aba0c862
commit d45131bbe0
22 changed files with 542 additions and 25 deletions
+4 -2
View File
@@ -70,8 +70,10 @@ const (
)
type ContainerSecurityContext struct {
RunAsUser *int64 `json:"run_as_user,omitempty"`
RunAsGroup *int64 `json:"run_as_group,omitempty"`
RunAsUser *int64 `json:"run_as_user,omitempty"`
RunAsGroup *int64 `json:"run_as_group,omitempty"`
SupplementalGroups []int64 `json:"supplemental_groups,omitempty"`
SupplementalGroupNames []string `json:"supplemental_group_names,omitempty"`
// procMount denotes the type of proc mount to use for the containers.
// The default is DefaultProcMount which uses the container runtime defaults for
ProcMount ContainerProcMountType `json:"proc_mount"`
+1
View File
@@ -80,6 +80,7 @@ func (s PortMappings) IsZero() bool {
type Device struct {
DevType string `json:"dev_type"`
SharingMode string `json:"sharing_mode,omitempty"`
Vendor string `json:"vendor,omitempty"`
Model string `json:"model"`
DevicePath string `json:"device_path"`
// MemoryMb is optional per-device VRAM (MiB) for HAMI. When > 0 it is used
+20 -4
View File
@@ -877,6 +877,10 @@ type PCIDevModelTypes struct {
VirtualDev bool
Hypervisor string
Vendor string `json:"vendor,allowempty"`
VendorDeviceId string `json:"vendor_device_id,allowempty"`
PciId string `json:"pci_id,allowempty"`
}
func getIsolatedDeviceInfo(ctx context.Context, userCred mcclient.TokenCredential, region *SCloudregion, zone *SZone, domainId, tenantId string) ([]string, []PCIDevModelTypes) {
@@ -918,7 +922,7 @@ func getIsolatedDeviceInfo(ctx context.Context, userCred mcclient.TokenCredentia
devices := devicesQ.SubQuery()
hosts := hostQuery.SubQuery()
q := devices.Query(hosts.Field("host_type"), devices.Field("model"), devices.Field("dev_type"), devices.Field("sharing_mode"), devices.Field("nvme_size_mb"), devices.Field("memory_size"))
q := devices.Query(hosts.Field("host_type"), devices.Field("vendor_device_id"), devices.Field("model"), devices.Field("dev_type"), devices.Field("sharing_mode"), devices.Field("nvme_size_mb"), devices.Field("memory_size"))
q = q.Filter(sqlchemy.NotIn(devices.Field("dev_type"), []string{api.USB_TYPE, api.NIC_TYPE}))
if zone != nil {
q = q.Join(hosts, sqlchemy.Equals(devices.Field("host_id"), hosts.Field("id")))
@@ -937,7 +941,7 @@ func getIsolatedDeviceInfo(ctx context.Context, userCred mcclient.TokenCredentia
sqlchemy.IsNullOrEmpty(hosts.Field("manager_id")),
))
}*/
q = q.GroupBy(hosts.Field("host_type"), devices.Field("model"), devices.Field("dev_type"), devices.Field("sharing_mode"), devices.Field("nvme_size_mb"), devices.Field("memory_size"))
q = q.GroupBy(hosts.Field("host_type"), devices.Field("vendor_device_id"), devices.Field("model"), devices.Field("dev_type"), devices.Field("sharing_mode"), devices.Field("nvme_size_mb"), devices.Field("memory_size"))
rows, err := q.Rows()
if err != nil {
@@ -949,12 +953,13 @@ func getIsolatedDeviceInfo(ctx context.Context, userCred mcclient.TokenCredentia
gpuModels := make([]string, 0)
for rows.Next() {
var m, t, sharingMode string
var vendorDeviceId string
var nvmeSizeMB int
var memSizeMB int
var vdev bool
var hypervisor string
var hostType string
rows.Scan(&hostType, &m, &t, &sharingMode, &nvmeSizeMB, &memSizeMB)
rows.Scan(&hostType, &vendorDeviceId, &m, &t, &sharingMode, &nvmeSizeMB, &memSizeMB)
if m == "" {
continue
@@ -972,7 +977,18 @@ func getIsolatedDeviceInfo(ctx context.Context, userCred mcclient.TokenCredentia
hypervisor = api.HYPERVISOR_ZETTAKIT
}
gpus = append(gpus, PCIDevModelTypes{m, t, sharingMode, nvmeSizeMB, memSizeMB, vdev, hypervisor})
gpus = append(gpus, PCIDevModelTypes{
Model: m,
DevType: t,
SharingMode: sharingMode,
NvmeSizeMB: nvmeSizeMB,
DevMemorySize: memSizeMB,
VirtualDev: vdev,
Hypervisor: hypervisor,
Vendor: GetVendorByVendorDeviceId(vendorDeviceId),
VendorDeviceId: vendorDeviceId,
PciId: vendorDeviceId,
})
if !utils.IsInStringArray(m, gpuModels) {
gpuModels = append(gpuModels, m)
+7
View File
@@ -2139,6 +2139,13 @@ func (s *sPodGuestInstance) createContainer(ctx context.Context, userCred mcclie
LocalhostRef: secInput.ApparmorProfile,
}
}
supplementalGroups, err := resolveSupplementalGroups(secInput.SupplementalGroups, secInput.SupplementalGroupNames)
if err != nil {
return "", errors.Wrap(err, "resolve supplemental groups")
}
if len(supplementalGroups) > 0 {
ctrCfg.Linux.SecurityContext.SupplementalGroups = supplementalGroups
}
}
if spec.EnableLxcfs {
+87
View File
@@ -0,0 +1,87 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package guestman
import (
"os/user"
"strconv"
"strings"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/util/procutils"
)
func parseGroupEntLine(line string) (int64, error) {
line = strings.TrimSpace(line)
if line == "" {
return 0, errors.Error("empty group entry")
}
parts := strings.Split(line, ":")
if len(parts) < 3 {
return 0, errors.Errorf("invalid group entry: %q", line)
}
gid, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return 0, errors.Wrapf(err, "parse gid from group entry %q", line)
}
return gid, nil
}
func lookupHostGroupGID(name string) (int64, error) {
name = strings.TrimSpace(name)
if name == "" {
return 0, errors.Error("group name is empty")
}
out, err := procutils.NewRemoteCommandAsFarAsPossible("getent", "group", name).Output()
if err == nil {
if gid, parseErr := parseGroupEntLine(string(out)); parseErr == nil {
return gid, nil
}
}
grp, lookupErr := user.LookupGroup(name)
if lookupErr != nil {
return 0, errors.Wrapf(lookupErr, "lookup group %q", name)
}
gid, err := strconv.ParseInt(grp.Gid, 10, 64)
if err != nil {
return 0, errors.Wrapf(err, "parse gid for group %q", name)
}
return gid, nil
}
func resolveSupplementalGroups(gids []int64, names []string) ([]int64, error) {
seen := make(map[int64]struct{}, len(gids)+len(names))
out := make([]int64, 0, len(gids)+len(names))
for _, gid := range gids {
if _, ok := seen[gid]; ok {
continue
}
seen[gid] = struct{}{}
out = append(out, gid)
}
for _, name := range names {
gid, err := lookupHostGroupGID(name)
if err != nil {
return nil, errors.Wrapf(err, "resolve supplemental group %q", name)
}
if _, ok := seen[gid]; ok {
continue
}
seen[gid] = struct{}{}
out = append(out, gid)
}
return out, nil
}
+36
View File
@@ -0,0 +1,36 @@
package guestman
import (
"reflect"
"testing"
)
func Test_parseGroupEntLine(t *testing.T) {
gid, err := parseGroupEntLine("video:x:44:")
if err != nil {
t.Fatalf("parseGroupEntLine: %v", err)
}
if gid != 44 {
t.Fatalf("gid = %d, want 44", gid)
}
}
func Test_resolveSupplementalGroups(t *testing.T) {
out, err := resolveSupplementalGroups([]int64{44, 100}, []string{})
if err != nil {
t.Fatalf("resolveSupplementalGroups: %v", err)
}
want := []int64{44, 100}
if !reflect.DeepEqual(out, want) {
t.Fatalf("got %v, want %v", out, want)
}
out, err = resolveSupplementalGroups([]int64{44}, nil)
if err != nil {
t.Fatalf("resolveSupplementalGroups single gid: %v", err)
}
want = []int64{44}
if !reflect.DeepEqual(out, want) {
t.Fatalf("single gid got %v, want %v", out, want)
}
}
+10 -1
View File
@@ -2690,9 +2690,14 @@ func (h *SHostInfo) injectTelegrafDeviceConfig(conf map[string]interface{}) {
// group dev
hasNetint := false
hasVasmi := false
hasHygon := false
hasNvidiasmi := false
hasNpusmi := false
for _, dev := range devs {
vendorId := strings.Split(dev.GetVendorDeviceId(), ":")[0]
if vendorId == api.HYGON_VENDOR_ID {
hasHygon = true
}
if !utils.IsInStringArray(dev.GetSharingMode(), api.VIRTUAL_SHARING_MODES) {
continue
}
@@ -2701,7 +2706,6 @@ func (h *SHostInfo) injectTelegrafDeviceConfig(conf map[string]interface{}) {
continue
}
vendorId := strings.Split(dev.GetVendorDeviceId(), ":")[0]
switch vendorId {
case api.AMD_VENDOR_ID:
confMap, ok := conf[system_service.TELEGRAF_INPUT_RADEONTOP].(map[string]interface{})
@@ -2735,6 +2739,11 @@ func (h *SHostInfo) injectTelegrafDeviceConfig(conf map[string]interface{}) {
system_service.TELEGRAF_INPUT_CONF_BIN_PATH: "/usr/bin/vasmi",
}
}
if hasHygon {
conf[system_service.TELEGRAF_INPUT_HYSMI] = map[string]interface{}{
system_service.TELEGRAF_INPUT_CONF_BIN_PATH: options.HostOptions.HygonHySmiPath,
}
}
if hasNvidiasmi {
conf[system_service.TELEGRAF_INPUT_NVIDIASMI] = struct{}{}
}
@@ -118,7 +118,7 @@ func (m *hygonDCUManager) GetContainerExtraConfigures(devs []*hostapi.ContainerD
func buildHygonRuntimeMounts() []*runtimeapi.Mount {
hyhalPath := options.HostOptions.HygonHyhalPath
dtkPath := options.HostOptions.HygonDtkPath
//dtkPath := options.HostOptions.HygonDtkPath
mounts := []*runtimeapi.Mount{}
if hygonPathExists(hyhalPath) {
mounts = append(mounts, &runtimeapi.Mount{
@@ -127,21 +127,21 @@ func buildHygonRuntimeMounts() []*runtimeapi.Mount {
Readonly: true,
})
}
if hygonPathExists(dtkPath) {
/*if hygonPathExists(dtkPath) {
mounts = append(mounts, &runtimeapi.Mount{
ContainerPath: dtkPath,
HostPath: dtkPath,
Readonly: true,
})
}
}*/
return mounts
}
func buildHygonRuntimeEnvs(indices []string) []*runtimeapi.KeyValue {
hyhalPath := options.HostOptions.HygonHyhalPath
dtkPath := options.HostOptions.HygonDtkPath
// hyhalPath := options.HostOptions.HygonHyhalPath
// dtkPath := options.HostOptions.HygonDtkPath
envs := []*runtimeapi.KeyValue{
{
/*{
Key: "HYGON_VISIBLE_DEVICES",
Value: strings.Join(indices, ","),
},
@@ -160,7 +160,7 @@ func buildHygonRuntimeEnvs(indices []string) []*runtimeapi.KeyValue {
{
Key: "ROCM_SMI_LIB_PATH",
Value: path.Join(hyhalPath, "lib"),
},
},*/
}
return envs
}
+8
View File
@@ -37,6 +37,7 @@ const (
TELEGRAF_INPUT_CONF_BIN_PATH = "bin_path"
TELEGRAF_INPUT_NETDEV = "ni_rsrc_mon"
TELEGRAF_INPUT_VASMI = "vasmi"
TELEGRAF_INPUT_HYSMI = "hysmi"
TELEGRAF_INPUT_NVIDIASMI = "nvidia-smi"
TELEGRAF_INPUT_NPUSMI = "npu-smi"
)
@@ -338,6 +339,13 @@ func (s *STelegraf) GetConfig(kwargs map[string]interface{}) string {
conf += "\n"
}
if hysmi, ok := kwargs[TELEGRAF_INPUT_HYSMI]; ok {
hysmiMap, _ := hysmi.(map[string]interface{})
conf += fmt.Sprintf("[[inputs.%s]]\n", TELEGRAF_INPUT_HYSMI)
conf += fmt.Sprintf(" bin_path = \"%s\"\n", hysmiMap[TELEGRAF_INPUT_CONF_BIN_PATH].(string))
conf += "\n"
}
if _, ok := kwargs[TELEGRAF_INPUT_NVIDIASMI]; ok {
conf += "[[inputs.nvidia_smi]]\n"
conf += "\n"
@@ -9,16 +9,19 @@ import (
)
func TestBuildVLLMEntrypointScriptMountedModelsFlag(t *testing.T) {
sleepScript := buildVLLMEntrypointScript("", 1, nil, nil)
sleepScript := buildVLLMEntrypointScript("", 1, nil, nil, false)
if !strings.Contains(sleepScript, "sleep infinity") {
t.Fatalf("expected idle script without model path, got %q", sleepScript)
}
if strings.Contains(sleepScript, "find ") {
t.Fatalf("idle script should not find models, got %q", sleepScript)
}
if strings.Contains(sleepScript, "/opt/dtk/env.sh") {
t.Fatalf("idle script should not source dtk env, got %q", sleepScript)
}
nested := "/data/models/huggingface/Qwen3-8B"
serveScript := buildVLLMEntrypointScript(nested, 1, nil, &api.LLMSpecVllm{PreferredModel: "Qwen3-8B"})
serveScript := buildVLLMEntrypointScript(nested, 1, nil, &api.LLMSpecVllm{PreferredModel: "Qwen3-8B"}, false)
if strings.Contains(serveScript, "sleep infinity") {
t.Fatalf("expected serve script with model path, got %q", serveScript)
}
@@ -28,6 +31,9 @@ func TestBuildVLLMEntrypointScriptMountedModelsFlag(t *testing.T) {
if !strings.Contains(serveScript, nested) {
t.Fatalf("expected nested model path in serve script, got %q", serveScript)
}
if strings.Contains(serveScript, "/opt/dtk/env.sh") {
t.Fatalf("non-hygon serve script should not source dtk env, got %q", serveScript)
}
if strings.Contains(serveScript, "find ") {
t.Fatalf("serve script should not find under MODELS_PATH, got %q", serveScript)
}
@@ -36,6 +42,25 @@ func TestBuildVLLMEntrypointScriptMountedModelsFlag(t *testing.T) {
}
}
func TestBuildVLLMEntrypointScriptHygonSourcesDTKEnv(t *testing.T) {
modelPath := "/data/models/huggingface/Qwen3-8B"
script := buildVLLMEntrypointScript(modelPath, 2, nil, nil, true)
if !strings.Contains(script, "/opt/dtk/env.sh") {
t.Fatalf("expected dtk env source, got %q", script)
}
if !strings.Contains(script, "/opt/dtk-*/env.sh") {
t.Fatalf("expected dtk version fallback, got %q", script)
}
if !strings.Contains(script, api.LLM_VLLM_EXEC_PATH) {
t.Fatalf("expected vllm exec after env source, got %q", script)
}
idx := strings.Index(script, "/opt/dtk/env.sh")
execIdx := strings.Index(script, "exec "+api.LLM_VLLM_EXEC_PATH)
if idx < 0 || execIdx < 0 || idx > execIdx {
t.Fatalf("expected env source before exec vllm, got %q", script)
}
}
func TestBuildSGLangEntrypointScriptNestedModelPath(t *testing.T) {
sleepScript := buildSGLangEntrypointScript("", 1, nil, nil)
if !strings.Contains(sleepScript, "sleep infinity") {
@@ -79,7 +104,7 @@ func TestLocalPathSkuEnablesServeEntrypoint(t *testing.T) {
if modelPath != "/data/models/huggingface/Qwen3-8B" {
t.Fatalf("expected nested local_path mount, got %q", modelPath)
}
script := buildVLLMEntrypointScript(modelPath, 1, nil, nil)
script := buildVLLMEntrypointScript(modelPath, 1, nil, nil, false)
if !strings.Contains(script, modelPath) {
t.Fatalf("expected script to embed mount path, got %q", script)
}
+34 -6
View File
@@ -198,16 +198,31 @@ func buildVLLMServeFlags(modelPath string, tensorParallelSize int, backendParame
)
}
func buildVLLMEntrypointScript(modelPath string, tensorParallelSize int, backendParameters []string, effSpec *api.LLMSpecVllm) string {
func buildHygonVLLMEnvSourceLines() []string {
return []string{
"if [ -f /opt/dtk/env.sh ]; then",
" . /opt/dtk/env.sh",
"else",
" _dtk_env=$(ls /opt/dtk-*/env.sh 2>/dev/null | head -1)",
" if [ -n \"$_dtk_env\" ] && [ -f \"$_dtk_env\" ]; then",
" . \"$_dtk_env\"",
" fi",
"fi",
}
}
func buildVLLMEntrypointScript(modelPath string, tensorParallelSize int, backendParameters []string, effSpec *api.LLMSpecVllm, hygon bool) string {
modelPath = strings.TrimSpace(modelPath)
if modelPath == "" {
return "exec sleep infinity"
}
serveCmd := strings.Join(buildVLLMServeFlags(modelPath, tensorParallelSize, backendParameters, effSpec), " ")
return strings.Join([]string{
"set -e",
fmt.Sprintf("exec %s %s", api.LLM_VLLM_EXEC_PATH, serveCmd),
}, "\n")
lines := []string{"set -e"}
if hygon {
lines = append(lines, buildHygonVLLMEnvSourceLines()...)
}
lines = append(lines, fmt.Sprintf("exec %s %s", api.LLM_VLLM_EXEC_PATH, serveCmd))
return strings.Join(lines, "\n")
}
func (v *vllm) GetSpec(sku *models.SLLMSku) interface{} {
@@ -414,7 +429,8 @@ func (v *vllm) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *mo
}
modelPath := models.PickContainerModelMountPath(models.CollectContainerModelMountPaths(llm, sku), preferred)
hasMountedModels := modelPath != "" || len(postOverlays) > 0 || models.SkuHasLocalHostPathModel(sku)
startScript := buildVLLMEntrypointScript(modelPath, tensorParallelSize, backendParameters, effSpec)
hygon := models.HasHygonDevices(llm, sku)
startScript := buildVLLMEntrypointScript(modelPath, tensorParallelSize, backendParameters, effSpec, hygon)
envs := []*commonapi.ContainerKeyValue{
{
Key: "HUGGING_FACE_HUB_CACHE",
@@ -446,6 +462,9 @@ func (v *vllm) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *mo
Envs: envs,
},
}
if hygon {
spec.Command = []string{"/bin/bash", "-c"}
}
if hasMountedModels {
spec.StartupProbe = newLLMHTTPStartupProbe(api.LLM_VLLM_DEFAULT_PORT, "/v1/models")
}
@@ -453,6 +472,15 @@ func (v *vllm) GetContainerSpec(ctx context.Context, llm *models.SLLM, image *mo
// GPU Devices
appendContainerIsolatedDevices(&spec, llm, sku, devices)
if hygon {
spec.Capabilities = &commonapi.ContainerCapability{
Add: []string{"SYS_PTRACE"},
}
spec.SecurityContext = &commonapi.ContainerSecurityContext{
SupplementalGroupNames: []string{"video"},
}
}
// Volume Mounts
diskIndex := 0
ctrVols := []*commonapi.ContainerVolumeMount{
@@ -0,0 +1,84 @@
package llm_container
import (
"context"
"reflect"
"strings"
"testing"
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
api "yunion.io/x/onecloud/pkg/apis/llm"
"yunion.io/x/onecloud/pkg/llm/models"
)
func TestVLLMGetContainerSpecHygonRuntime(t *testing.T) {
v := newVLLM().(*vllm)
hostPaths := api.HostPaths{
{
Type: "directory",
Path: "/data/models/Qwen3-8B",
Containers: api.ContainerHostPathRelations{
"0": {MountPath: "/data/models/huggingface/Qwen3-8B"},
},
},
}
hygonSku := &models.SLLMSku{
SLLMSkuBase: models.SLLMSkuBase{
Devices: &api.Devices{
{DevType: computeapi.CONTAINER_DEV_HYGON_DCU},
{DevType: computeapi.CONTAINER_DEV_HYGON_DCU},
},
},
LLMType: string(api.LLM_CONTAINER_VLLM),
Source: api.LLM_MODEL_SOURCE_LOCAL_PATH,
LocalPath: "/data/models/Qwen3-8B",
}
hygonSku.HostPaths = &hostPaths
image := &models.SLLMImage{}
out := v.GetContainerSpec(context.Background(), nil, image, hygonSku, nil, nil, "")
if out == nil {
t.Fatal("expected container spec")
}
spec := &out.ContainerSpec
if spec.Capabilities == nil || !reflect.DeepEqual(spec.Capabilities.Add, []string{"SYS_PTRACE"}) {
t.Fatalf("capabilities = %#v, want SYS_PTRACE", spec.Capabilities)
}
if spec.SecurityContext == nil || !reflect.DeepEqual(spec.SecurityContext.SupplementalGroupNames, []string{"video"}) {
t.Fatalf("security context = %#v, want video group", spec.SecurityContext)
}
if len(spec.Command) != 2 || spec.Command[0] != "/bin/bash" || spec.Command[1] != "-c" {
t.Fatalf("command = %#v, want [/bin/bash -c]", spec.Command)
}
if len(spec.Args) == 0 || !strings.Contains(spec.Args[0], "/opt/dtk/env.sh") {
t.Fatalf("args = %#v, want dtk env source in entrypoint", spec.Args)
}
}
func TestVLLMGetContainerSpecNvidiaNoHygonRuntime(t *testing.T) {
v := newVLLM().(*vllm)
nvSku := &models.SLLMSku{
SLLMSkuBase: models.SLLMSkuBase{
Devices: &api.Devices{
{DevType: computeapi.CONTAINER_DEV_NVIDIA_GPU},
},
},
}
image := &models.SLLMImage{}
out := v.GetContainerSpec(context.Background(), nil, image, nvSku, nil, nil, "")
if out == nil {
t.Fatal("expected container spec")
}
spec := &out.ContainerSpec
if spec.Capabilities != nil {
t.Fatalf("expected nil capabilities, got %#v", spec.Capabilities)
}
if spec.SecurityContext != nil {
t.Fatalf("expected nil security context, got %#v", spec.SecurityContext)
}
if len(spec.Command) != 2 || spec.Command[0] != "/bin/sh" {
t.Fatalf("command = %#v, want [/bin/sh -c]", spec.Command)
}
if len(spec.Args) > 0 && strings.Contains(spec.Args[0], "/opt/dtk/env.sh") {
t.Fatalf("args should not source dtk env for nvidia, got %#v", spec.Args)
}
}
+19
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"yunion.io/x/jsonutils"
@@ -97,6 +98,24 @@ func getEffectiveDevices(llmBase *SLLMBase, skuBase *SLLMSkuBase) *api.Devices {
return nil
}
// HasHygonDevices reports whether effective devices include Hygon DCU (exclusive or HAMI).
func HasHygonDevices(llm *SLLM, sku *SLLMSku) bool {
devs := GetEffectiveDevices(llm, sku)
if devs == nil {
return false
}
for _, d := range *devs {
if strings.EqualFold(d.Vendor, "HYGON") {
return true
}
switch d.DevType {
case computeapi.CONTAINER_DEV_HYGON_DCU, computeapi.CONTAINER_DEV_HYGON_DCU_HAMI:
return true
}
}
return false
}
// GetEffectiveHostPaths returns the host_paths to apply with llm's override taking priority over sku.
func GetEffectiveHostPaths(llm *SLLM, sku *SLLMSku) *api.HostPaths {
var llmBase *SLLMBase
+1
View File
@@ -99,6 +99,7 @@ func GetLLMBasePodCreateInput(
isolatedDevice := &computeapi.IsolatedDeviceConfig{
DevType: devices[i].DevType,
SharingMode: devices[i].SharingMode,
Vendor: devices[i].Vendor,
Model: devices[i].Model,
DevicePath: devices[i].DevicePath,
MemoryMb: memMb,
+1
View File
@@ -662,6 +662,7 @@ func buildIsolatedDeviceMemoryParams(device api.Device) *jsonutils.JSONDict {
params.Set("show_baremetal_isolated_devices", jsonutils.JSONTrue)
setStringArrayParam(params, "dev_type", dev.DevType)
setStringArrayParam(params, "sharing_mode", dev.SharingMode)
setStringArrayParam(params, "vendor", dev.Vendor)
setStringArrayParam(params, "model", dev.Model)
setStringArrayParam(params, "device_path", dev.DevicePath)
return params
+65
View File
@@ -0,0 +1,65 @@
package models
import (
"testing"
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
api "yunion.io/x/onecloud/pkg/apis/llm"
)
func TestHasHygonDevices(t *testing.T) {
hygonSku := &SLLMSku{
SLLMSkuBase: SLLMSkuBase{
Devices: &api.Devices{
{DevType: computeapi.CONTAINER_DEV_HYGON_DCU},
},
},
}
if !HasHygonDevices(nil, hygonSku) {
t.Fatal("expected Hygon DCU sku to be detected")
}
hamiSku := &SLLMSku{
SLLMSkuBase: SLLMSkuBase{
Devices: &api.Devices{
{DevType: computeapi.CONTAINER_DEV_HYGON_DCU_HAMI},
},
},
}
if !HasHygonDevices(nil, hamiSku) {
t.Fatal("expected Hygon DCU HAMI sku to be detected")
}
nvSku := &SLLMSku{
SLLMSkuBase: SLLMSkuBase{
Devices: &api.Devices{
{DevType: computeapi.CONTAINER_DEV_NVIDIA_GPU},
},
},
}
if HasHygonDevices(nil, nvSku) {
t.Fatal("expected NVIDIA sku not to be detected as Hygon")
}
llm := &SLLM{
SLLMBase: SLLMBase{
Devices: &api.Devices{
{DevType: computeapi.CONTAINER_DEV_HYGON_DCU},
},
},
}
if !HasHygonDevices(llm, nvSku) {
t.Fatal("expected llm device override to win over sku")
}
normalizedHygonSku := &SLLMSku{
SLLMSkuBase: SLLMSkuBase{
Devices: &api.Devices{
{DevType: computeapi.GPU_TYPE, Vendor: "HYGON", Model: "BW"},
},
},
}
if !HasHygonDevices(nil, normalizedHygonSku) {
t.Fatal("expected normalized GPU+HYGON vendor sku to be detected as Hygon")
}
}
+28
View File
@@ -2,6 +2,7 @@ package models
import (
"context"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
@@ -95,7 +96,24 @@ func normalizeLLMSkuDevices(devices *api.Devices) error {
return nil
}
func canonicalizeLLMDeviceVendor(vendor string) string {
vendor = strings.TrimSpace(vendor)
if vendor == "" {
return ""
}
for name := range computeapi.VENDOR_ID_MAP {
if strings.EqualFold(name, vendor) {
return name
}
}
if name, ok := computeapi.ID_VENDOR_MAP[vendor]; ok {
return name
}
return strings.ToUpper(vendor)
}
func normalizeLLMSkuDevice(dev *api.Device) {
origDevType := dev.DevType
switch dev.DevType {
case "":
dev.DevType = computeapi.GPU_TYPE
@@ -133,6 +151,16 @@ func normalizeLLMSkuDevice(dev *api.Device) {
if dev.SharingMode == "" {
dev.SharingMode = computeapi.DEVICE_SHARING_MODE_HAMI
}
if dev.Vendor == "" {
switch origDevType {
case computeapi.CONTAINER_DEV_HYGON_DCU, computeapi.CONTAINER_DEV_HYGON_DCU_HAMI:
dev.Vendor = "HYGON"
case computeapi.CONTAINER_DEV_NVIDIA_GPU, computeapi.CONTAINER_DEV_NVIDIA_MPS,
computeapi.CONTAINER_DEV_NVIDIA_GPU_SHARE, computeapi.CONTAINER_DEV_NVIDIA_HAMI:
dev.Vendor = "NVIDIA"
}
}
dev.Vendor = canonicalizeLLMDeviceVendor(dev.Vendor)
}
func (skuBase *SLLMSkuBase) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.LLMSkuBaseUpdateInput) (api.LLMSkuBaseUpdateInput, error) {
+38
View File
@@ -25,48 +25,63 @@ func TestNormalizeLLMSkuDeviceLegacyTypes(t *testing.T) {
in api.Device
wantDevType string
wantSharingMode string
wantVendor string
}{
{
name: "NVIDIA_GPU_SHARE",
in: api.Device{DevType: computeapi.CONTAINER_DEV_NVIDIA_GPU_SHARE},
wantDevType: computeapi.GPU_TYPE,
wantSharingMode: computeapi.DEVICE_SHARING_MODE_UNLIMITED,
wantVendor: "NVIDIA",
},
{
name: "NVIDIA_MPS",
in: api.Device{DevType: computeapi.CONTAINER_DEV_NVIDIA_MPS},
wantDevType: computeapi.GPU_TYPE,
wantSharingMode: computeapi.DEVICE_SHARING_MODE_MPS,
wantVendor: "NVIDIA",
},
{
name: "NVIDIA_GPU",
in: api.Device{DevType: computeapi.CONTAINER_DEV_NVIDIA_GPU},
wantDevType: computeapi.GPU_TYPE,
wantSharingMode: computeapi.DEVICE_SHARING_MODE_EXCLUSIVE,
wantVendor: "NVIDIA",
},
{
name: "NVIDIA_HAMI",
in: api.Device{DevType: computeapi.CONTAINER_DEV_NVIDIA_HAMI},
wantDevType: computeapi.GPU_TYPE,
wantSharingMode: computeapi.DEVICE_SHARING_MODE_HAMI,
wantVendor: "NVIDIA",
},
{
name: "HYGON_DCU",
in: api.Device{DevType: computeapi.CONTAINER_DEV_HYGON_DCU},
wantDevType: computeapi.GPU_TYPE,
wantSharingMode: computeapi.DEVICE_SHARING_MODE_EXCLUSIVE,
wantVendor: "HYGON",
},
{
name: "HYGON_DCU_HAMI",
in: api.Device{DevType: computeapi.CONTAINER_DEV_HYGON_DCU_HAMI},
wantDevType: computeapi.GPU_TYPE,
wantSharingMode: computeapi.DEVICE_SHARING_MODE_HAMI,
wantVendor: "HYGON",
},
{
name: "explicit sharing_mode preserved",
in: api.Device{DevType: computeapi.CONTAINER_DEV_NVIDIA_GPU_SHARE, SharingMode: computeapi.DEVICE_SHARING_MODE_MPS},
wantDevType: computeapi.GPU_TYPE,
wantSharingMode: computeapi.DEVICE_SHARING_MODE_MPS,
wantVendor: "NVIDIA",
},
{
name: "explicit vendor preserved and canonicalized",
in: api.Device{DevType: computeapi.GPU_TYPE, Vendor: "hygon", Model: "BW"},
wantDevType: computeapi.GPU_TYPE,
wantSharingMode: computeapi.DEVICE_SHARING_MODE_HAMI,
wantVendor: "HYGON",
},
}
for _, tc := range cases {
@@ -79,6 +94,9 @@ func TestNormalizeLLMSkuDeviceLegacyTypes(t *testing.T) {
if dev.SharingMode != tc.wantSharingMode {
t.Fatalf("SharingMode = %q, want %q", dev.SharingMode, tc.wantSharingMode)
}
if dev.Vendor != tc.wantVendor {
t.Fatalf("Vendor = %q, want %q", dev.Vendor, tc.wantVendor)
}
})
}
}
@@ -266,6 +284,7 @@ func TestLLMPodIsolatedDeviceConfigFromSKU(t *testing.T) {
out = append(out, &computeapi.IsolatedDeviceConfig{
DevType: devs[i].DevType,
SharingMode: devs[i].SharingMode,
Vendor: devs[i].Vendor,
Model: devs[i].Model,
DevicePath: devs[i].DevicePath,
MemoryMb: perDev,
@@ -279,6 +298,9 @@ func TestLLMPodIsolatedDeviceConfigFromSKU(t *testing.T) {
if out[1].DevType != computeapi.GPU_TYPE || out[1].SharingMode != computeapi.DEVICE_SHARING_MODE_UNLIMITED {
t.Fatalf("device1 = %#v", out[1])
}
if out[1].Vendor != "NVIDIA" {
t.Fatalf("device1 Vendor = %q, want NVIDIA", out[1].Vendor)
}
if out[0].MemoryRequest != perDev || out[1].MemoryRequest != perDev {
t.Fatalf("MemoryRequest = %d,%d want %d", out[0].MemoryRequest, out[1].MemoryRequest, perDev)
}
@@ -315,3 +337,19 @@ func TestBuildIsolatedDeviceMemoryParamsExclusiveUsesUnused(t *testing.T) {
t.Fatal("exclusive params should set unused")
}
}
func TestBuildIsolatedDeviceMemoryParamsVendor(t *testing.T) {
params := buildIsolatedDeviceMemoryParams(api.Device{
DevType: computeapi.GPU_TYPE,
SharingMode: computeapi.DEVICE_SHARING_MODE_EXCLUSIVE,
Model: "BW",
Vendor: "HYGON",
})
vendors, err := params.GetArray("vendor")
if err != nil || len(vendors) != 1 {
t.Fatalf("vendor = %v err=%v", params, err)
}
if s, _ := vendors[0].GetString(); s != "HYGON" {
t.Fatalf("vendor = %q", s)
}
}
+7 -2
View File
@@ -24,7 +24,7 @@ type LLMSkuBaseCreateOptions struct {
// DiskOverlay string `help:"disk overlay, e.g. /opt/steam-data/base:/opt/steam-data/games"`
TemplateId string
PortMappings []string `help:"port mapping in the format of protocol:port[:prefix][:first_port_offset][:env_key=env_value], e.g. tcp:5555:192.168.0.0/16:5:WOLF_BASE_PORT=20000"`
Devices []string `help:"device info in the format of model[:path[:dev_type[:sharing_mode]]], e.g. 'GeForce RTX 4060'"`
Devices []string `help:"device info in the format of model[:path[:dev_type[:sharing_mode[:vendor]]]], e.g. 'GeForce RTX 4060' or 'BW::GPU:EXCLUSIVE:HYGON'"`
HostPaths []string `json:"-" help:"host path mount in format path=<host_path>,type=<directory|file>,container_index=<index>,mount_path=<container_path>[,auto_create=<bool>][,read_only=<bool>][,propagation=<private|rslave|rshared>][,fs_user=<uid>][,fs_group=<gid>][,uid=<uid>][,gid=<gid>][,permissions=<mode>]; repeatable"`
Env []string `help:"env in format of key=value"`
@@ -69,7 +69,7 @@ type LLMSkuBaseUpdateOptions struct {
// Dpi *int
// Fps *int
PortMappings []string `help:"port mapping in the format of protocol:port[:prefix][:first_port_offset], e.g. tcp:5555:192.168.0.0/16,10.10.0.0/16:1000"`
Devices []string `help:"device info in the format of model[:path[:dev_type[:sharing_mode]]], e.g. QuadraT2A:/dev/nvme1n1, Device::VASTAITECH_GPU"`
Devices []string `help:"device info in the format of model[:path[:dev_type[:sharing_mode[:vendor]]]], e.g. QuadraT2A:/dev/nvme1n1, BW::GPU:EXCLUSIVE:HYGON"`
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"`
@@ -365,6 +365,7 @@ func fetchDevices(devStrs []string, dict *jsonutils.JSONDict) {
devpath := ""
devType := ""
sharingMode := ""
vendor := ""
if len(segs) > 1 {
devpath = segs[1]
}
@@ -374,11 +375,15 @@ func fetchDevices(devStrs []string, dict *jsonutils.JSONDict) {
if len(segs) > 3 {
sharingMode = segs[3]
}
if len(segs) > 4 {
vendor = segs[4]
}
devs = append(devs, api.Device{
Model: segs[0],
DevicePath: devpath,
DevType: devType,
SharingMode: sharingMode,
Vendor: vendor,
})
}
}
+1
View File
@@ -31,6 +31,7 @@ var All = []SMeasurement{
system,
vasmi,
npuSmi,
hysmi,
worker,
serviceHttpCode,
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package measurements
import "yunion.io/x/onecloud/pkg/apis/monitor"
var hysmi = SMeasurement{
Context: []SMonitorContext{
{
"hysmi", "Hygon DCU metrics",
monitor.METRIC_RES_TYPE_HOST, monitor.METRIC_DATABASE_TELE,
},
},
Metrics: []SMetric{
{
"temperature_gpu", "DCU temperature", "",
},
{
"power_draw", "DCU power draw", "",
},
{
"power_cap", "DCU power cap", "",
},
{
"utilization_gpu", "DCU utilization", monitor.METRIC_UNIT_PERCENT,
},
{
"utilization_memory", "DCU memory utilization", monitor.METRIC_UNIT_PERCENT,
},
{
"utilization_encoder", "DCU encoder utilization", monitor.METRIC_UNIT_PERCENT,
},
{
"utilization_decoder", "DCU decoder utilization", monitor.METRIC_UNIT_PERCENT,
},
},
}
@@ -257,6 +257,13 @@ Measurement,MeasurementNote,ResourceType,Database,Metric,MetricNote,MetricUnit
"npu_smi","Ascend NPU metrics","host","telegraf","aicube_usage_rate","NPU aicube usage rate","%s"
"npu_smi","Ascend NPU metrics","host","telegraf","aicpu_usage_rate","NPU aicpu usage rate","%s"
"npu_smi","Ascend NPU metrics","host","telegraf","hbm_bandwidth_usage_rate","NPU hbm bandwidth usage rate","%s"
"hysmi","Hygon HCU metrics","host","telegraf","temperature_gpu","HCU temperature",""
"hysmi","Hygon HCU metrics","host","telegraf","power_draw","HCU power draw",""
"hysmi","Hygon HCU metrics","host","telegraf","power_cap","HCU power cap",""
"hysmi","Hygon HCU metrics","host","telegraf","utilization_gpu","HCU utilization","%"
"hysmi","Hygon HCU metrics","host","telegraf","utilization_memory","HCU memory utilization","%"
"hysmi","Hygon HCU metrics","host","telegraf","utilization_encoder","HCU encoder utilization","%"
"hysmi","Hygon HCU metrics","host","telegraf","utilization_decoder","HCU decoder utilization","%"
"worker","Worker queue","system","system","active_worker_cnt","Active Worker Count","NULL"
"worker","Worker queue","system","system","max_worker_count","Max Worker Count","NULL"
"worker","Worker queue","system","system","detach_worker_cnt","Detach worker Count","NULL"
1 Measurement MeasurementNote ResourceType Database Metric MetricNote MetricUnit
257 npu_smi Ascend NPU metrics host telegraf aicube_usage_rate NPU aicube usage rate %s
258 npu_smi Ascend NPU metrics host telegraf aicpu_usage_rate NPU aicpu usage rate %s
259 npu_smi Ascend NPU metrics host telegraf hbm_bandwidth_usage_rate NPU hbm bandwidth usage rate %s
260 hysmi Hygon HCU metrics host telegraf temperature_gpu HCU temperature
261 hysmi Hygon HCU metrics host telegraf power_draw HCU power draw
262 hysmi Hygon HCU metrics host telegraf power_cap HCU power cap
263 hysmi Hygon HCU metrics host telegraf utilization_gpu HCU utilization %
264 hysmi Hygon HCU metrics host telegraf utilization_memory HCU memory utilization %
265 hysmi Hygon HCU metrics host telegraf utilization_encoder HCU encoder utilization %
266 hysmi Hygon HCU metrics host telegraf utilization_decoder HCU decoder utilization %
267 worker Worker queue system system active_worker_cnt Active Worker Count NULL
268 worker Worker queue system system max_worker_count Max Worker Count NULL
269 worker Worker queue system system detach_worker_cnt Detach worker Count NULL