Files
cloudpods/pkg/llm/models/llm_benchmark.go
2026-07-15 15:00:37 +08:00

1419 lines
50 KiB
Go

package models
import (
"context"
"database/sql"
stderrors "errors"
"os"
"path/filepath"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/httputils"
"yunion.io/x/pkg/utils"
"yunion.io/x/sqlchemy"
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
imageapi "yunion.io/x/onecloud/pkg/apis/image"
api "yunion.io/x/onecloud/pkg/apis/llm"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/httperrors"
bench "yunion.io/x/onecloud/pkg/llm/benchmark"
"yunion.io/x/onecloud/pkg/llm/options"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules/compute"
"yunion.io/x/onecloud/pkg/util/logclient"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
func init() {
GetLLMBenchmarkManager()
}
var llmBenchmarkManager *SLLMBenchmarkManager
func GetLLMBenchmarkManager() *SLLMBenchmarkManager {
if llmBenchmarkManager != nil {
return llmBenchmarkManager
}
llmBenchmarkManager = &SLLMBenchmarkManager{
SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager(
SLLMBenchmark{},
"llm_benchmarks_tbl",
"llm_benchmark",
"llm_benchmarks",
),
}
llmBenchmarkManager.SetVirtualObject(llmBenchmarkManager)
return llmBenchmarkManager
}
type SLLMBenchmarkManager struct {
db.SVirtualResourceBaseManager
}
type LLMBenchmarkTokenizerMount struct {
ImageId string
ModelPath string
SizeMB int
MountBase string
MountSubdirectory string
}
func selectBenchmarkTokenizerModel(llmType, model string, mounted []*SInstantModel) (*SInstantModel, error) {
matched := make([]*SInstantModel, 0, 1)
for _, candidate := range mounted {
if upstreamModelKeyFromInstantModel(llmType, candidate) == strings.TrimSpace(model) {
matched = append(matched, candidate)
}
}
if len(matched) == 1 {
return matched[0], nil
}
if len(matched) > 1 {
return nil, httperrors.NewInputParameterError("multiple mounted models match %s", model)
}
if len(mounted) == 1 {
return mounted[0], nil
}
return nil, httperrors.NewInputParameterError("cannot uniquely resolve tokenizer model %s", model)
}
func buildBenchmarkTokenizerMount(llmType string, model *SInstantModel) (*LLMBenchmarkTokenizerMount, error) {
if model == nil || model.ImageId == "" || model.Status != imageapi.IMAGE_STATUS_ACTIVE || model.GetActualSizeMb() <= 0 {
return nil, httperrors.NewInvalidStatusError("benchmark tokenizer model image is not active")
}
subdirectory := ""
switch strings.ToLower(strings.TrimSpace(llmType)) {
case string(api.LLM_CONTAINER_VLLM):
subdirectory = api.LLM_VLLM
case string(api.LLM_CONTAINER_SGLANG):
subdirectory = api.LLM_SGLANG
default:
return nil, httperrors.NewInputParameterError("offline synthetic tokenizer is unsupported for %s", llmType)
}
modelPath := ""
for _, mount := range model.Mounts {
clean := filepath.Clean(mount)
rel, err := filepath.Rel(api.LLM_VLLM_BASE_PATH, clean)
if err == nil && rel != "." && rel != ".." && !filepath.IsAbs(rel) && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
modelPath = clean
break
}
}
if modelPath == "" {
return nil, httperrors.NewInvalidStatusError("benchmark tokenizer model has no mount below %s", api.LLM_VLLM_BASE_PATH)
}
return &LLMBenchmarkTokenizerMount{
ImageId: model.ImageId,
ModelPath: modelPath,
SizeMB: int(model.GetActualSizeMb()) + 512,
MountBase: api.LLM_VLLM_BASE_PATH,
MountSubdirectory: subdirectory,
}, nil
}
func resolveBenchmarkTokenizerMount(llm *SLLM, model string) (*LLMBenchmarkTokenizerMount, error) {
boolTrue := true
relations, err := llm.FetchModels(nil, &boolTrue, nil)
if err != nil {
return nil, errors.Wrap(err, "fetch mounted models")
}
mounted := make([]*SInstantModel, 0, len(relations))
for i := range relations {
instant, err := GetInstantModelManager().GetInstantModelById(relations[i].InstantModelId)
if err != nil {
return nil, errors.Wrap(err, "fetch mounted instant model")
}
mounted = append(mounted, instant)
}
llmType := string(llm.GetLLMContainerDriver().GetType())
selected, err := selectBenchmarkTokenizerModel(llmType, model, mounted)
if err != nil {
return nil, err
}
return buildBenchmarkTokenizerMount(llmType, selected)
}
type SLLMBenchmark struct {
db.SVirtualResourceBase
LLMId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"`
LLMDeploymentId string `width:"128" charset:"ascii" nullable:"true" list:"user" create:"optional" index:"true"`
LLMSkuId string `width:"128" charset:"ascii" nullable:"true" list:"user" create:"optional" index:"true"`
LLMImageId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"optional" index:"true"`
BenchmarkPackageId string `width:"128" charset:"ascii" nullable:"true" list:"user" create:"optional" index:"true"`
Backend string `width:"64" charset:"ascii" nullable:"true" list:"user" create:"optional"`
Model string `width:"256" charset:"utf8" nullable:"true" list:"user" create:"optional" update:"user"`
TargetUrl string `charset:"utf8" length:"medium" nullable:"true" list:"user" create:"optional"`
RequestFormat string `width:"64" charset:"ascii" nullable:"true" list:"user" create:"optional"`
Profile string `width:"32" charset:"ascii" nullable:"false" default:"constant" list:"user" create:"optional"`
DatasetName string `width:"64" charset:"ascii" nullable:"false" default:"synthetic_text" list:"user" create:"optional"`
DatasetInputTokens int `nullable:"true" list:"user" create:"optional" update:"user"`
DatasetOutputTokens int `nullable:"true" list:"user" create:"optional" update:"user"`
DatasetPath string `charset:"utf8" length:"medium" nullable:"true" list:"user" create:"optional"`
RequestRate int `nullable:"true" list:"user" create:"optional" update:"user"`
TotalRequests int `nullable:"true" list:"user" create:"optional" update:"user"`
MaxDurationSeconds int `nullable:"true" list:"user" create:"optional" update:"user"`
MaxErrors int `nullable:"true" list:"user" create:"optional" update:"user"`
State string `width:"32" charset:"ascii" nullable:"false" default:"pending" list:"user" create:"optional" update:"user" index:"true"`
StateMessage string `charset:"utf8" length:"medium" nullable:"true" list:"user" create:"optional" update:"user"`
TaskId string `width:"128" charset:"ascii" nullable:"true" list:"user" update:"user"`
StopRequested bool `nullable:"false" default:"false" list:"user" update:"user"`
RunnerServerId string `width:"128" charset:"ascii" nullable:"true" list:"user" update:"user"`
RunnerContainerId string `width:"128" charset:"ascii" nullable:"true" list:"user" update:"user"`
WorkDir string `charset:"utf8" length:"medium" nullable:"true" list:"user" create:"optional"`
LogPath string `charset:"utf8" length:"medium" nullable:"true" list:"user" update:"user"`
ResultJson string `charset:"utf8" length:"medium" nullable:"true" list:"user" update:"user"`
ResultCsv string `charset:"utf8" length:"medium" nullable:"true" list:"user" update:"user"`
DatasetPreflight *api.LLMBenchmarkDatasetPreflight `json:"dataset_preflight,omitempty" length:"long" nullable:"true" list:"user" update:"user"`
PreflightLogPath string `charset:"utf8" length:"medium" nullable:"true" update:"user"`
PreflightResultJson string `charset:"utf8" length:"medium" nullable:"true" update:"user"`
RawPreflightLog string `charset:"utf8" length:"long" nullable:"true" update:"user"`
RawPreflightResult string `charset:"utf8" length:"long" nullable:"true" update:"user"`
DatasetEvaluation *api.LLMBenchmarkDatasetEvaluation `json:"dataset_evaluation,omitempty" length:"long" nullable:"true" list:"user" update:"user"`
EvaluationResultJson string `charset:"utf8" length:"medium" nullable:"true" update:"user"`
EvaluationResultCsv string `charset:"utf8" length:"medium" nullable:"true" update:"user"`
EvaluationLogPath string `charset:"utf8" length:"medium" nullable:"true" update:"user"`
ArtifactStorage string `width:"32" charset:"ascii" nullable:"true" list:"user" update:"user"`
ArtifactStorageMessage string `charset:"utf8" length:"medium" nullable:"true" list:"user" update:"user"`
TargetSnapshot string `charset:"utf8" length:"long" nullable:"true" list:"user" create:"optional" update:"user"`
GuideLLMSpec string `charset:"utf8" length:"long" nullable:"true" list:"user" create:"optional" update:"user"`
RawMetrics string `charset:"utf8" length:"long" nullable:"true" list:"user" update:"user"`
RawLog string `charset:"utf8" length:"long" nullable:"true" update:"user"`
RawCsv string `charset:"utf8" length:"long" nullable:"true" update:"user"`
RequestsPerSecondMean float64 `nullable:"true" list:"user" update:"user"`
RequestLatencyMeanSec float64 `nullable:"true" list:"user" update:"user"`
RequestTotal int `nullable:"true" list:"user" update:"user"`
RequestSuccessful int `nullable:"true" list:"user" update:"user"`
RequestErrored int `nullable:"true" list:"user" update:"user"`
ErrorRate float64 `nullable:"true" list:"user" update:"user"`
}
func (man *SLLMBenchmarkManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input *api.LLMBenchmarkCreateInput) (*api.LLMBenchmarkCreateInput, error) {
var err error
input.VirtualResourceCreateInput, err = man.SVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.VirtualResourceCreateInput)
if err != nil {
return input, errors.Wrap(err, "validate VirtualResourceCreateInput")
}
return prepareLLMBenchmarkCreateInput(ctx, userCred, ownerId, input)
}
func prepareLLMBenchmarkCreateInput(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, input *api.LLMBenchmarkCreateInput) (*api.LLMBenchmarkCreateInput, error) {
llm, dep, err := resolveBenchmarkTarget(ctx, userCred, input)
if err != nil {
return input, err
}
sku, err := llm.GetLLMSku(llm.LLMSkuId)
if err != nil {
return input, errors.Wrap(err, "GetLLMSku")
}
image, err := resolveBenchmarkImage(ctx, userCred, input.BenchmarkImage)
if err != nil {
return input, err
}
pkg, err := resolveBenchmarkPackage(ctx, userCred, input.BenchmarkPackage)
if err != nil {
return input, err
}
if pkg != nil {
benchmarkProjectID := ""
if ownerId != nil {
benchmarkProjectID = ownerId.GetProjectId()
}
if err := validateBenchmarkPackageProject(pkg.ProjectId, benchmarkProjectID); err != nil {
return input, err
}
}
access, err := llm.GetLLMAccessUrlInfo(ctx, userCred, jsonutils.NewDict())
if err != nil {
return input, errors.Wrap(err, "GetLLMAccessUrlInfo")
}
targetURL := ""
if access != nil {
targetURL = access.InternalUrl
if targetURL == "" {
targetURL = access.LoginUrl
}
}
if targetURL == "" {
return input, errors.Wrap(httperrors.ErrInvalidStatus, "target url is empty")
}
defaultLLMBenchmarkInput(input, pkg)
if err := validateLLMBenchmarkInput(input, pkg != nil); err != nil {
return input, err
}
if input.Model == "" {
input.Model = resolveBenchmarkModel(ctx, targetURL, sku.LLMType, llm)
}
if pkg == nil {
if _, err := resolveBenchmarkTokenizerMount(llm, input.Model); err != nil {
return input, errors.Wrap(err, "resolve offline benchmark tokenizer")
}
}
input.LLMId = llm.Id
input.LLMSkuId = sku.Id
input.LLMImageId = image.Id
input.BenchmarkPackageId = ""
if pkg != nil {
input.BenchmarkPackageId = pkg.Id
}
input.LLMDeploymentId = llm.LLMDeploymentId
if dep != nil {
input.LLMDeploymentId = dep.Id
}
input.Backend = sku.LLMType
input.TargetUrl = targetURL
workDir := benchmarkWorkDirRoot()
input.WorkDir = filepath.Join(workDir, input.Name)
if !benchmarkWorkDirIsSafe(workDir, input.WorkDir) {
return input, httperrors.NewInputParameterError("name produces unsafe benchmark workdir")
}
input.TargetSnapshot = jsonutils.Marshal(map[string]interface{}{
"llm_id": llm.Id,
"llm_name": llm.Name,
"llm_status": llm.Status,
"llm_deployment_id": input.LLMDeploymentId,
"llm_sku_id": sku.Id,
"llm_sku_name": sku.Name,
"backend": sku.LLMType,
"llm_image_id": image.Id,
"llm_image_name": image.Name,
"benchmark_package_id": input.BenchmarkPackageId,
"target_url": targetURL,
"request_format": input.RequestFormat,
"model": input.Model,
"request_rate": input.RequestRate,
"total_requests": input.TotalRequests,
}).String()
spec := bench.BuildGuideLLMSpec(bench.GuideLLMSpecInput{
TargetURL: input.TargetUrl,
RequestFormat: input.RequestFormat,
Model: input.Model,
RequestRate: input.RequestRate,
TotalRequests: input.TotalRequests,
MaxDurationSeconds: input.MaxDurationSeconds,
MaxErrors: input.MaxErrors,
DatasetInputTokens: input.DatasetInputTokens,
DatasetOutputTokens: input.DatasetOutputTokens,
DatasetPath: input.DatasetPath,
})
input.GuideLLMSpec = jsonutils.Marshal(spec).String()
return input, nil
}
func benchmarkWorkDirRoot() string {
if root := options.Options.LLMBenchmarkWorkDir; root != "" {
return root
}
return "/opt/cloud/workspace/llm/benchmarks"
}
func benchmarkWorkDirIsSafe(root, path string) bool {
rel, err := filepath.Rel(filepath.Clean(root), filepath.Clean(path))
return err == nil && rel != "." && rel != ".." && !filepath.IsAbs(rel) &&
!strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
func resolveBenchmarkTarget(ctx context.Context, userCred mcclient.TokenCredential, input *api.LLMBenchmarkCreateInput) (*SLLM, *SLLMDeployment, error) {
if input.LLMId != "" && input.LLMDeploymentId != "" {
return nil, nil, errors.Wrap(httperrors.ErrInputParameter, "llm_id and llm_deployment_id are mutually exclusive")
}
if input.LLMDeploymentId != "" {
depObj, err := GetLLMDeploymentManager().FetchByIdOrName(ctx, userCred, input.LLMDeploymentId)
if err != nil {
return nil, nil, errors.Wrap(err, "fetch LLMDeployment")
}
dep := depObj.(*SLLMDeployment)
llm := &SLLM{}
err = GetLLMManager().Query().
Equals("llm_deployment_id", dep.Id).
Equals("status", api.LLM_STATUS_RUNNING).
Asc("created_at").
First(llm)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, nil, errors.Wrapf(httperrors.ErrInvalidStatus, "deployment %s has no running llm", dep.Name)
}
return nil, nil, errors.Wrap(err, "query running deployment LLM")
}
llm.SetModelManager(GetLLMManager(), llm)
return llm, dep, nil
}
if input.LLMId == "" {
return nil, nil, errors.Wrap(httperrors.ErrMissingParameter, "llm_id or llm_deployment_id")
}
llmObj, err := GetLLMManager().FetchByIdOrName(ctx, userCred, input.LLMId)
if err != nil {
return nil, nil, errors.Wrap(err, "fetch LLM")
}
llm := llmObj.(*SLLM)
if llm.Status != api.LLM_STATUS_RUNNING {
return nil, nil, errors.Wrapf(httperrors.ErrInvalidStatus, "llm %s status is %s", llm.Name, llm.Status)
}
return llm, nil, nil
}
func resolveBenchmarkModel(ctx context.Context, targetURL string, llmType string, llm *SLLM) string {
if providerType, ok := benchmarkProviderType(llmType); ok {
out, err := GetLLMManager().performProviderModels(ctx, api.LLMProviderModelsInput{
URL: targetURL,
ProviderType: providerType,
})
if err == nil {
for _, model := range out.Models {
if model = strings.TrimSpace(model); model != "" {
return model
}
}
}
}
if infos, err := llm.FetchMountedModelInfo(); err == nil && len(infos) > 0 {
return upstreamModelKeyFromMountedInfo(llmType, &infos[0])
}
return ""
}
func benchmarkProviderType(llmType string) (api.LLMClientType, bool) {
switch strings.ToLower(strings.TrimSpace(llmType)) {
case string(api.LLM_CONTAINER_OLLAMA):
return api.LLM_CLIENT_OLLAMA, true
case string(api.LLM_CONTAINER_VLLM), string(api.LLM_CONTAINER_SGLANG):
return api.LLM_CLIENT_OPENAI, true
default:
return "", false
}
}
func defaultLLMBenchmarkInput(input *api.LLMBenchmarkCreateInput, pkg *SLLMBenchmarkPackage) {
if input.RequestFormat == "" {
input.RequestFormat = api.LLMBenchmarkDefaultRequestFormat
}
if input.Profile == "" {
input.Profile = api.LLMBenchmarkProfileConstant
}
if pkg != nil {
if input.DatasetName == "" || input.DatasetName == api.LLMBenchmarkDatasetSyntheticText {
input.DatasetName = api.LLMBenchmarkDatasetPackage
}
if input.DatasetPath == "" {
input.DatasetPath = pkg.DatasetPath
}
} else if input.DatasetName == "" {
input.DatasetName = api.LLMBenchmarkDatasetSyntheticText
}
if input.RequestRate <= 0 {
input.RequestRate = options.Options.LLMBenchmarkDefaultRequestRate
if input.RequestRate <= 0 {
input.RequestRate = 1
}
}
if input.TotalRequests <= 0 {
input.TotalRequests = options.Options.LLMBenchmarkDefaultTotalRequests
if input.TotalRequests <= 0 {
input.TotalRequests = 100
}
}
if input.MaxDurationSeconds <= 0 {
input.MaxDurationSeconds = 600
}
if input.MaxErrors <= 0 {
input.MaxErrors = 10
}
if input.DatasetInputTokens <= 0 {
input.DatasetInputTokens = options.Options.LLMBenchmarkDefaultInputTokens
if input.DatasetInputTokens <= 0 {
input.DatasetInputTokens = 1024
}
}
if input.DatasetOutputTokens <= 0 {
input.DatasetOutputTokens = options.Options.LLMBenchmarkDefaultOutputTokens
if input.DatasetOutputTokens <= 0 {
input.DatasetOutputTokens = 128
}
}
}
func validateLLMBenchmarkInput(input *api.LLMBenchmarkCreateInput, hasPackage bool) error {
if input.RequestFormat != api.LLMBenchmarkDefaultRequestFormat {
return errors.Wrap(httperrors.ErrInputParameter, "request_format only supports "+api.LLMBenchmarkDefaultRequestFormat)
}
if input.Profile != api.LLMBenchmarkProfileConstant {
return errors.Wrap(httperrors.ErrInputParameter, "profile only supports constant")
}
if hasPackage {
if input.DatasetName != api.LLMBenchmarkDatasetPackage {
return errors.Wrap(httperrors.ErrInputParameter, "dataset_name must be benchmark_package when benchmark_package is set")
}
if strings.TrimSpace(input.DatasetPath) == "" {
return httperrors.NewMissingParameterError("dataset_path")
}
} else if input.DatasetName != api.LLMBenchmarkDatasetSyntheticText {
return errors.Wrap(httperrors.ErrInputParameter, "dataset_name only supports synthetic_text")
}
if max := options.Options.LLMBenchmarkMaxRequestRate; max > 0 && input.RequestRate > max {
return errors.Wrapf(httperrors.ErrInputParameter, "request_rate must be <= %d", max)
}
if max := options.Options.LLMBenchmarkMaxTotalRequests; max > 0 && input.TotalRequests > max {
return errors.Wrapf(httperrors.ErrInputParameter, "total_requests must be <= %d", max)
}
if max := options.Options.LLMBenchmarkMaxDurationSeconds; max > 0 && input.MaxDurationSeconds > max {
return errors.Wrapf(httperrors.ErrInputParameter, "max_duration_seconds must be <= %d", max)
}
return nil
}
func validateBenchmarkMutableState(state string) error {
if utils.IsInStringArray(state, []string{
api.LLMBenchmarkStateCompleted,
api.LLMBenchmarkStateStopped,
api.LLMBenchmarkStateError,
}) {
return nil
}
return httperrors.NewInvalidStatusError("benchmark is %s", state)
}
func (b *SLLMBenchmark) ValidateUpdateCondition(ctx context.Context) error {
return validateBenchmarkMutableState(b.State)
}
func positiveBenchmarkUpdate(name string, value *int) error {
if value != nil && *value <= 0 {
return httperrors.NewInputParameterError("%s must be greater than 0", name)
}
return nil
}
func (b *SLLMBenchmark) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.LLMBenchmarkUpdateInput) (api.LLMBenchmarkUpdateInput, error) {
var err error
input.VirtualResourceBaseUpdateInput, err = b.SVirtualResourceBase.ValidateUpdateData(ctx, userCred, query, input.VirtualResourceBaseUpdateInput)
if err != nil {
return input, errors.Wrap(err, "ValidateUpdateData")
}
if err := validateBenchmarkMutableState(b.State); err != nil {
return input, err
}
for name, value := range map[string]*int{
"request_rate": input.RequestRate,
"total_requests": input.TotalRequests,
"max_duration_seconds": input.MaxDurationSeconds,
"max_errors": input.MaxErrors,
"dataset_input_tokens": input.DatasetInputTokens,
"dataset_output_tokens": input.DatasetOutputTokens,
} {
if err := positiveBenchmarkUpdate(name, value); err != nil {
return input, err
}
}
if b.BenchmarkPackageId != "" && (input.DatasetInputTokens != nil || input.DatasetOutputTokens != nil) {
return input, httperrors.NewInputParameterError("dataset token fields only apply to synthetic_text")
}
candidate := benchmarkCreateInputFromModel(b)
applyLLMBenchmarkUpdateInput(candidate, input)
if err := validateLLMBenchmarkInput(candidate, b.BenchmarkPackageId != ""); err != nil {
return input, err
}
if b.BenchmarkPackageId == "" && benchmarkUpdateChangesRunConfig(input) {
obj, err := GetLLMManager().FetchById(b.LLMId)
if err != nil {
return input, errors.Wrap(err, "fetch benchmark LLM")
}
if _, err := resolveBenchmarkTokenizerMount(obj.(*SLLM), candidate.Model); err != nil {
return input, errors.Wrap(err, "resolve offline benchmark tokenizer")
}
}
return input, nil
}
func benchmarkCreateInputFromModel(b *SLLMBenchmark) *api.LLMBenchmarkCreateInput {
ret := &api.LLMBenchmarkCreateInput{
LLMId: b.LLMId,
LLMDeploymentId: b.LLMDeploymentId,
LLMImageId: b.LLMImageId,
BenchmarkPackageId: b.BenchmarkPackageId,
RequestFormat: b.RequestFormat,
Model: b.Model,
Profile: b.Profile,
RequestRate: b.RequestRate,
TotalRequests: b.TotalRequests,
MaxDurationSeconds: b.MaxDurationSeconds,
MaxErrors: b.MaxErrors,
DatasetName: b.DatasetName,
DatasetInputTokens: b.DatasetInputTokens,
DatasetOutputTokens: b.DatasetOutputTokens,
DatasetPath: b.DatasetPath,
Backend: b.Backend,
TargetUrl: b.TargetUrl,
WorkDir: b.WorkDir,
TargetSnapshot: b.TargetSnapshot,
GuideLLMSpec: b.GuideLLMSpec,
}
ret.Name = b.Name
ret.Description = b.Description
return ret
}
func applyLLMBenchmarkUpdateInput(candidate *api.LLMBenchmarkCreateInput, input api.LLMBenchmarkUpdateInput) {
if input.Model != nil {
candidate.Model = strings.TrimSpace(*input.Model)
}
if input.RequestRate != nil {
candidate.RequestRate = *input.RequestRate
}
if input.TotalRequests != nil {
candidate.TotalRequests = *input.TotalRequests
}
if input.MaxDurationSeconds != nil {
candidate.MaxDurationSeconds = *input.MaxDurationSeconds
}
if input.MaxErrors != nil {
candidate.MaxErrors = *input.MaxErrors
}
if input.DatasetInputTokens != nil {
candidate.DatasetInputTokens = *input.DatasetInputTokens
}
if input.DatasetOutputTokens != nil {
candidate.DatasetOutputTokens = *input.DatasetOutputTokens
}
}
func benchmarkUpdateChangesRunConfig(input api.LLMBenchmarkUpdateInput) bool {
return input.Model != nil ||
input.RequestRate != nil ||
input.TotalRequests != nil ||
input.MaxDurationSeconds != nil ||
input.MaxErrors != nil ||
input.DatasetInputTokens != nil ||
input.DatasetOutputTokens != nil
}
func buildLLMBenchmarkUpdateData(b *SLLMBenchmark, input api.LLMBenchmarkUpdateInput) *jsonutils.JSONDict {
if !benchmarkUpdateChangesRunConfig(input) {
return jsonutils.NewDict()
}
candidate := benchmarkCreateInputFromModel(b)
applyLLMBenchmarkUpdateInput(candidate, input)
spec := bench.BuildGuideLLMSpec(bench.GuideLLMSpecInput{
TargetURL: candidate.TargetUrl,
RequestFormat: candidate.RequestFormat,
Model: candidate.Model,
RequestRate: candidate.RequestRate,
TotalRequests: candidate.TotalRequests,
MaxDurationSeconds: candidate.MaxDurationSeconds,
MaxErrors: candidate.MaxErrors,
DatasetInputTokens: candidate.DatasetInputTokens,
DatasetOutputTokens: candidate.DatasetOutputTokens,
DatasetPath: candidate.DatasetPath,
})
snapshot, _ := jsonutils.ParseString(b.TargetSnapshot)
snapshotDict, ok := snapshot.(*jsonutils.JSONDict)
if !ok {
snapshotDict = jsonutils.NewDict()
}
snapshotDict.Set("model", jsonutils.NewString(candidate.Model))
snapshotDict.Set("request_rate", jsonutils.NewInt(int64(candidate.RequestRate)))
snapshotDict.Set("total_requests", jsonutils.NewInt(int64(candidate.TotalRequests)))
data := jsonutils.NewDict()
if input.Model != nil {
data.Set("model", jsonutils.NewString(candidate.Model))
}
if input.RequestRate != nil {
data.Set("request_rate", jsonutils.NewInt(int64(candidate.RequestRate)))
}
if input.TotalRequests != nil {
data.Set("total_requests", jsonutils.NewInt(int64(candidate.TotalRequests)))
}
if input.MaxDurationSeconds != nil {
data.Set("max_duration_seconds", jsonutils.NewInt(int64(candidate.MaxDurationSeconds)))
}
if input.MaxErrors != nil {
data.Set("max_errors", jsonutils.NewInt(int64(candidate.MaxErrors)))
}
if input.DatasetInputTokens != nil {
data.Set("dataset_input_tokens", jsonutils.NewInt(int64(candidate.DatasetInputTokens)))
}
if input.DatasetOutputTokens != nil {
data.Set("dataset_output_tokens", jsonutils.NewInt(int64(candidate.DatasetOutputTokens)))
}
data.Set("guide_llm_spec", jsonutils.NewString(jsonutils.Marshal(spec).String()))
data.Set("target_snapshot", jsonutils.NewString(snapshotDict.String()))
data.Set("state", jsonutils.NewString(api.LLMBenchmarkStateStopped))
for _, field := range []string{
"state_message", "task_id", "runner_server_id", "runner_container_id",
"log_path", "result_json", "result_csv", "raw_metrics", "raw_log", "raw_csv",
"preflight_log_path", "preflight_result_json",
"raw_preflight_log", "raw_preflight_result",
"evaluation_result_json", "evaluation_result_csv", "evaluation_log_path",
"artifact_storage", "artifact_storage_message",
} {
data.Set(field, jsonutils.NewString(""))
}
data.Set("dataset_preflight", jsonutils.JSONNull)
data.Set("dataset_evaluation", jsonutils.JSONNull)
data.Set("stop_requested", jsonutils.JSONFalse)
for _, field := range []string{
"requests_per_second_mean", "request_latency_mean_sec", "error_rate",
} {
data.Set(field, jsonutils.NewFloat64(0))
}
for _, field := range []string{
"request_total", "request_successful", "request_errored",
} {
data.Set(field, jsonutils.NewInt(0))
}
return data
}
func sanitizeLLMBenchmarkUpdateData(data *jsonutils.JSONDict) {
for _, field := range []string{
"state", "state_message", "task_id", "stop_requested",
"runner_server_id", "runner_container_id",
"log_path", "result_json", "result_csv",
"dataset_preflight", "preflight_log_path", "preflight_result_json",
"raw_preflight_log", "raw_preflight_result",
"dataset_evaluation", "evaluation_result_json", "evaluation_result_csv", "evaluation_log_path",
"artifact_storage", "artifact_storage_message",
"target_snapshot", "guide_llm_spec", "raw_metrics", "raw_log", "raw_csv",
"requests_per_second_mean", "request_latency_mean_sec",
"request_total", "request_successful", "request_errored", "error_rate",
} {
data.RemoveIgnoreCase(field)
}
}
func (b *SLLMBenchmark) PreUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) {
dataDict := data.(*jsonutils.JSONDict)
input := api.LLMBenchmarkUpdateInput{}
_ = dataDict.Unmarshal(&input)
if benchmarkUpdateChangesRunConfig(input) {
if err := b.CleanupArtifacts(ctx); err != nil {
log.Warningf("cleanup benchmark %s artifacts before update: %s", b.Id, err)
}
}
sanitizeLLMBenchmarkUpdateData(dataDict)
dataDict.Update(buildLLMBenchmarkUpdateData(b, input))
b.SVirtualResourceBase.PreUpdate(ctx, userCred, query, data)
}
func resolveBenchmarkPackage(ctx context.Context, userCred mcclient.TokenCredential, packageName string) (*SLLMBenchmarkPackage, error) {
if packageName == "" {
return nil, nil
}
obj, err := GetLLMBenchmarkPackageManager().FetchByIdOrName(ctx, userCred, packageName)
if err != nil {
return nil, errors.Wrap(err, "fetch benchmark package")
}
pkg := obj.(*SLLMBenchmarkPackage)
if pkg.ImageId == "" {
return nil, httperrors.NewInvalidStatusError("benchmark package %s has no image", pkg.Name)
}
if pkg.Status != imageapi.IMAGE_STATUS_ACTIVE {
return nil, httperrors.NewInvalidStatusError("benchmark package %s is %s", pkg.Name, pkg.Status)
}
return pkg, nil
}
func PrepareLLMBenchmarkCreateInput(pkg *SLLMBenchmarkPackage, spec *api.LLMBenchmarkCreateInput) (*api.LLMBenchmarkCreateInput, error) {
if spec == nil {
return nil, nil
}
if pkg == nil || pkg.Id == "" {
return nil, errors.Wrap(httperrors.ErrInputParameter, "empty benchmark package")
}
if pkg.Status != imageapi.IMAGE_STATUS_ACTIVE || pkg.ImageId == "" {
return nil, httperrors.NewInvalidStatusError("benchmark package %s is not active", pkg.Name)
}
input := *spec
input.BenchmarkPackage = pkg.Id
input.BenchmarkPackageId = pkg.Id
input.DatasetName = api.LLMBenchmarkDatasetPackage
input.DatasetPath = pkg.DatasetPath
return &input, nil
}
func validateBenchmarkPackageUnusedCount(count int) error {
if count > 0 {
return errors.Wrap(httperrors.ErrInvalidStatus, "benchmark package is still used by benchmarks")
}
return nil
}
func validateBenchmarkPackageProject(packageProjectID, benchmarkProjectID string) error {
if packageProjectID != benchmarkProjectID {
return httperrors.NewInputParameterError("benchmark package and benchmark must belong to the same project")
}
return nil
}
func CountBenchmarkPackageReferences(packageID, excludeBenchmarkID string) (int, error) {
if packageID == "" {
return 0, nil
}
q := GetLLMBenchmarkManager().Query().
Equals("benchmark_package_id", packageID).
IsFalse("deleted")
if excludeBenchmarkID != "" {
q = q.NotEquals("id", excludeBenchmarkID)
}
count, err := q.CountWithError()
if err != nil {
return 0, errors.Wrap(err, "count benchmark package references")
}
return count, nil
}
func ValidateBenchmarkPackageUnused(packageID, excludeBenchmarkID string) error {
count, err := CountBenchmarkPackageReferences(packageID, excludeBenchmarkID)
if err != nil {
return err
}
return validateBenchmarkPackageUnusedCount(count)
}
func buildLLMBenchmarkCopyCreateInput(source *SLLMBenchmark, input api.LLMBenchmarkCopyInput) (*api.LLMBenchmarkCreateInput, error) {
if strings.TrimSpace(input.Name) == "" {
return nil, httperrors.NewMissingParameterError("name")
}
if strings.TrimSpace(input.LLMDeploymentId) == "" {
return nil, httperrors.NewMissingParameterError("llm_deployment_id")
}
if source.BenchmarkPackageId != "" && (input.DatasetInputTokens != nil || input.DatasetOutputTokens != nil) {
return nil, httperrors.NewInputParameterError("dataset token fields only apply to synthetic_text")
}
ret := &api.LLMBenchmarkCreateInput{
LLMDeploymentId: input.LLMDeploymentId,
BenchmarkImage: source.LLMImageId,
BenchmarkPackage: source.BenchmarkPackageId,
RequestFormat: source.RequestFormat,
Profile: source.Profile,
RequestRate: source.RequestRate,
TotalRequests: source.TotalRequests,
MaxDurationSeconds: source.MaxDurationSeconds,
MaxErrors: source.MaxErrors,
DatasetName: source.DatasetName,
DatasetInputTokens: source.DatasetInputTokens,
DatasetOutputTokens: source.DatasetOutputTokens,
DatasetPath: source.DatasetPath,
}
ret.Name = input.Name
ret.Description = source.Description
if input.Description != nil {
ret.Description = *input.Description
}
if input.Model != nil {
ret.Model = strings.TrimSpace(*input.Model)
}
if input.RequestRate != nil {
ret.RequestRate = *input.RequestRate
}
if input.TotalRequests != nil {
ret.TotalRequests = *input.TotalRequests
}
if input.MaxDurationSeconds != nil {
ret.MaxDurationSeconds = *input.MaxDurationSeconds
}
if input.MaxErrors != nil {
ret.MaxErrors = *input.MaxErrors
}
if input.DatasetInputTokens != nil {
ret.DatasetInputTokens = *input.DatasetInputTokens
}
if input.DatasetOutputTokens != nil {
ret.DatasetOutputTokens = *input.DatasetOutputTokens
}
return ret, nil
}
func resolveBenchmarkImage(ctx context.Context, userCred mcclient.TokenCredential, imageName string) (*SLLMImage, error) {
var image *SLLMImage
if imageName != "" {
obj, err := GetLLMImageManager().FetchByIdOrName(ctx, userCred, imageName)
if err != nil {
return nil, errors.Wrap(err, "fetch benchmark image")
}
image = obj.(*SLLMImage)
} else {
defaultImage := options.Options.LLMBenchmarkDefaultImage
if defaultImage == "" {
defaultImage = api.LLMBenchmarkDefaultImage
}
name, label := parseImageRef(defaultImage)
image = &SLLMImage{}
err := GetLLMImageManager().Query().
Equals("image_name", name).
Equals("image_label", label).
Equals("llm_type", string(api.LLM_IMAGE_TYPE_BENCHMARK)).
First(image)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, httperrors.NewNotFoundError("default benchmark llm_image %s not found, create it first", defaultImage)
}
return nil, errors.Wrap(err, "query default benchmark image")
}
image.SetModelManager(GetLLMImageManager(), image)
}
if image.LLMType != string(api.LLM_IMAGE_TYPE_BENCHMARK) {
return nil, errors.Wrapf(httperrors.ErrInputParameter, "image %s is not benchmark type", image.Name)
}
return image, nil
}
func parseImageRef(ref string) (string, string) {
idx := strings.LastIndex(ref, ":")
if idx <= 0 {
return ref, "latest"
}
return ref[:idx], ref[idx+1:]
}
func (man *SLLMBenchmarkManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.LLMBenchmarkListInput) (*sqlchemy.SQuery, error) {
q, err := man.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, input.VirtualResourceListInput)
if err != nil {
return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemFilter")
}
if input.LLMId != "" {
q = q.Equals("llm_id", input.LLMId)
}
if input.LLMDeploymentId != "" {
depObj, err := GetLLMDeploymentManager().FetchByIdOrName(ctx, userCred, input.LLMDeploymentId)
if err != nil {
return nil, errors.Wrap(err, "fetch LLMDeployment")
}
q = q.Equals("llm_deployment_id", depObj.GetId())
}
if input.State != "" {
q = q.Equals("state", input.State)
}
return q, nil
}
func (man *SLLMBenchmarkManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) {
q, err := man.SVirtualResourceBaseManager.QueryDistinctExtraField(q, field)
if err == nil {
return q, nil
}
if !isLLMBenchmarkDeploymentExtraField(field) {
return q, httperrors.ErrNotFound
}
depQ := GetLLMDeploymentManager().Query("id", "name").Distinct().SubQuery()
q.AppendField(depQ.Field("name", field))
q = q.Join(depQ, sqlchemy.Equals(q.Field("llm_deployment_id"), depQ.Field("id")))
q.GroupBy(depQ.Field("name"))
return q, nil
}
func isLLMBenchmarkDeploymentExtraField(field string) bool {
return field == "llm_deployment"
}
func (man *SLLMBenchmarkManager) FetchCustomizeColumns(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
objs []interface{},
fields stringutils2.SSortedStrings,
isList bool,
) []api.LLMBenchmarkDetails {
virtRows := man.SVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
benchmarks := make([]SLLMBenchmark, len(objs))
jsonutils.Update(&benchmarks, objs)
rows := make([]api.LLMBenchmarkDetails, len(objs))
deploymentIds := make([]string, 0, len(objs))
for i := range rows {
rows[i].VirtualResourceDetails = virtRows[i]
if benchmarks[i].LLMDeploymentId != "" {
deploymentIds = append(deploymentIds, benchmarks[i].LLMDeploymentId)
}
}
if len(deploymentIds) == 0 {
return rows
}
deploymentNames, err := db.FetchIdNameMap2(GetLLMDeploymentManager(), deploymentIds)
if err != nil {
return rows
}
for i := range rows {
rows[i].LLMDeployment = deploymentNames[benchmarks[i].LLMDeploymentId]
}
return rows
}
func (man *SLLMBenchmarkManager) CreateAndStart(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, input *api.LLMBenchmarkCreateInput) (*SLLMBenchmark, error) {
data := jsonutils.Marshal(input)
obj, err := db.DoCreate(man, ctx, userCred, nil, data, ownerId)
if err != nil {
return nil, errors.Wrap(err, "DoCreate benchmark")
}
benchmark := obj.(*SLLMBenchmark)
func() {
lockman.LockObject(ctx, benchmark)
defer lockman.ReleaseObject(ctx, benchmark)
benchmark.PostCreate(ctx, userCred, ownerId, nil, data)
if err := man.GetExtraHook().AfterPostCreate(ctx, userCred, ownerId, benchmark, nil, data); err != nil {
logclient.AddActionLogWithContext(ctx, benchmark, logclient.ACT_POST_CREATE_HOOK, err, userCred, false)
}
}()
notes := benchmark.GetShortDesc(ctx)
db.OpsLog.LogEvent(benchmark, db.ACT_CREATE, notes, userCred)
logclient.AddActionLogWithContext(ctx, benchmark, logclient.ACT_CREATE, notes, userCred, true)
man.OnCreateComplete(ctx, []db.IModel{benchmark}, userCred, ownerId, nil, []jsonutils.JSONObject{data})
return benchmark, nil
}
func (b *SLLMBenchmark) PerformCopy(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.LLMBenchmarkCopyInput) (jsonutils.JSONObject, error) {
createInput, err := buildLLMBenchmarkCopyCreateInput(b, input)
if err != nil {
return nil, err
}
benchmark, err := GetLLMBenchmarkManager().CreateAndStart(ctx, userCred, b.GetOwnerId(), createInput)
if err != nil {
return nil, err
}
ret := jsonutils.NewDict()
ret.Set("benchmark_id", jsonutils.NewString(benchmark.Id))
ret.Set("state", jsonutils.NewString(benchmark.State))
return ret, nil
}
func (b *SLLMBenchmark) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
b.SVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
if err := b.StartRunTask(ctx, userCred, ""); err != nil {
_ = b.SetState(ctx, userCred, api.LLMBenchmarkStateError, err.Error())
}
}
func (b *SLLMBenchmark) SetState(ctx context.Context, userCred mcclient.TokenCredential, state string, message string) error {
_, err := db.Update(b, func() error {
b.State = state
b.StateMessage = message
return nil
})
return err
}
func (b *SLLMBenchmark) FinishRun(ctx context.Context, runErr error) (string, error) {
lockman.LockObject(ctx, b)
defer lockman.ReleaseObject(ctx, b)
obj, err := GetLLMBenchmarkManager().FetchById(b.Id)
if err != nil {
return "", err
}
current := obj.(*SLLMBenchmark)
state, message := benchmarkRunFinalState(current.State, current.StopRequested, runErr)
_, err = db.Update(current, func() error {
current.State = state
current.StateMessage = message
return nil
})
if err == nil {
b.State = state
b.StateMessage = message
b.StopRequested = current.StopRequested
}
return state, err
}
func (b *SLLMBenchmark) SetRunner(ctx context.Context, serverId string, containerId string) error {
_, err := db.Update(b, func() error {
b.RunnerServerId = serverId
b.RunnerContainerId = containerId
return nil
})
return err
}
func resetLLMBenchmarkResultFields(b *SLLMBenchmark) {
b.StateMessage = ""
b.TaskId = ""
b.StopRequested = false
b.RunnerServerId = ""
b.RunnerContainerId = ""
b.LogPath = ""
b.ResultJson = ""
b.ResultCsv = ""
b.RawMetrics = ""
b.RawLog = ""
b.RawCsv = ""
b.DatasetPreflight = nil
b.PreflightLogPath = ""
b.PreflightResultJson = ""
b.RawPreflightLog = ""
b.RawPreflightResult = ""
b.DatasetEvaluation = nil
b.EvaluationResultJson = ""
b.EvaluationResultCsv = ""
b.EvaluationLogPath = ""
b.ArtifactStorage = ""
b.ArtifactStorageMessage = ""
b.RequestsPerSecondMean = 0
b.RequestLatencyMeanSec = 0
b.RequestTotal = 0
b.RequestSuccessful = 0
b.RequestErrored = 0
b.ErrorRate = 0
}
func (b *SLLMBenchmark) StartRunTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error {
task, err := taskman.TaskManager.NewTask(ctx, "LLMBenchmarkRunTask", b, userCred, nil, parentTaskId, "", nil)
if err != nil {
return errors.Wrap(err, "NewTask")
}
if _, err := db.Update(b, func() error {
b.TaskId = task.GetId()
b.State = api.LLMBenchmarkStatePending
return nil
}); err != nil {
return err
}
return task.ScheduleRun(nil)
}
func (b *SLLMBenchmark) UpdateMetrics(ctx context.Context, userCred mcclient.TokenCredential, metrics *bench.LLMBenchmarkMetrics) error {
_, err := db.Update(b, func() error {
b.RequestsPerSecondMean = floatValue(metrics.RequestsPerSecondMean)
b.RequestLatencyMeanSec = floatValue(metrics.RequestLatencyMeanSec)
b.RequestTotal = metrics.RequestTotal
b.RequestSuccessful = metrics.RequestSuccessful
b.RequestErrored = metrics.RequestErrored
b.ErrorRate = floatValue(metrics.ErrorRate)
return nil
})
return err
}
func (b *SLLMBenchmark) UpdateDatasetPreflight(
ctx context.Context,
userCred mcclient.TokenCredential,
summary api.LLMBenchmarkDatasetPreflight,
resultPath, logPath string,
) error {
_, err := db.Update(b, func() error {
b.DatasetPreflight = &summary
b.PreflightResultJson = resultPath
b.PreflightLogPath = logPath
return nil
})
return err
}
func (b *SLLMBenchmark) UpdateDatasetEvaluation(
ctx context.Context,
userCred mcclient.TokenCredential,
summary api.LLMBenchmarkDatasetEvaluation,
resultJSON, resultCSV, logPath string,
) error {
_, err := db.Update(b, func() error {
b.DatasetEvaluation = &summary
b.EvaluationResultJson = resultJSON
b.EvaluationResultCsv = resultCSV
b.EvaluationLogPath = logPath
return nil
})
return err
}
func floatValue(v *float64) float64 {
if v == nil {
return 0
}
return *v
}
func (b *SLLMBenchmark) ArtifactPath(kind string) (string, error) {
switch kind {
case "preflight":
return b.PreflightResultJson, nil
case "preflight-log":
return b.PreflightLogPath, nil
case "log":
return b.LogPath, nil
case "json":
return b.ResultJson, nil
case "csv":
return b.ResultCsv, nil
case "evaluation":
return b.EvaluationResultJson, nil
case "evaluation-csv":
return b.EvaluationResultCsv, nil
case "evaluation-log":
return b.EvaluationLogPath, nil
default:
return "", httperrors.NewInputParameterError("unknown artifact type %s", kind)
}
}
func (b *SLLMBenchmark) ArtifactLocations() map[string]string {
return map[string]string{
"preflight": b.PreflightResultJson,
"preflight-log": b.PreflightLogPath,
"log": b.LogPath,
"json": b.ResultJson,
"csv": b.ResultCsv,
"evaluation": b.EvaluationResultJson,
"evaluation-csv": b.EvaluationResultCsv,
"evaluation-log": b.EvaluationLogPath,
}
}
func applyLLMBenchmarkArtifactLocations(b *SLLMBenchmark, locations map[string]string) {
b.PreflightResultJson = locations["preflight"]
b.PreflightLogPath = locations["preflight-log"]
b.LogPath = locations["log"]
b.ResultJson = locations["json"]
b.ResultCsv = locations["csv"]
b.EvaluationResultJson = locations["evaluation"]
b.EvaluationResultCsv = locations["evaluation-csv"]
b.EvaluationLogPath = locations["evaluation-log"]
}
func (b *SLLMBenchmark) UpdateArtifactLocations(
ctx context.Context,
userCred mcclient.TokenCredential,
locations map[string]string,
storage, message string,
) error {
_, err := db.Update(b, func() error {
applyLLMBenchmarkArtifactLocations(b, locations)
b.ArtifactStorage = storage
b.ArtifactStorageMessage = message
return nil
})
return err
}
func (b *SLLMBenchmark) CleanupArtifacts(ctx context.Context) error {
var firstErr error
if err := bench.DefaultArtifactStore().DeleteBenchmark(ctx, b.ProjectId, b.Id); err != nil {
firstErr = err
}
if b.WorkDir == "" {
return firstErr
}
if !benchmarkWorkDirIsSafe(benchmarkWorkDirRoot(), b.WorkDir) {
if firstErr != nil {
return firstErr
}
return errors.Errorf("unsafe benchmark workdir %s", b.WorkDir)
}
if err := os.RemoveAll(b.WorkDir); err != nil && firstErr == nil {
firstErr = err
}
return firstErr
}
func (b *SLLMBenchmark) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error {
if utils.IsInStringArray(b.State, []string{api.LLMBenchmarkStatePending, api.LLMBenchmarkStateQueued, api.LLMBenchmarkStateValidating, api.LLMBenchmarkStateRunning}) {
return httperrors.NewInvalidStatusError("benchmark is %s, stop it first", b.State)
}
return nil
}
func (b *SLLMBenchmark) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
return b.StartDeleteTask(ctx, userCred, "")
}
func (b *SLLMBenchmark) StartDeleteTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error {
task, err := taskman.TaskManager.NewTask(ctx, "LLMBenchmarkDeleteTask", b, userCred, nil, parentTaskId, "", nil)
if err != nil {
return errors.Wrap(err, "NewTask LLMBenchmarkDeleteTask")
}
return task.ScheduleRun(nil)
}
func (b *SLLMBenchmark) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
return nil
}
func (b *SLLMBenchmark) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
return b.SVirtualResourceBase.Delete(ctx, userCred)
}
func (b *SLLMBenchmark) PerformRetest(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.LLMBenchmarkRetestInput) (jsonutils.JSONObject, error) {
if err := validateBenchmarkMutableState(b.State); err != nil {
return nil, err
}
createInput := benchmarkCreateInputFromModel(b)
createInput.BenchmarkImage = b.LLMImageId
createInput.BenchmarkPackage = b.BenchmarkPackageId
if b.LLMDeploymentId != "" {
createInput.LLMId = ""
}
prepared, err := prepareLLMBenchmarkCreateInput(ctx, userCred, b.GetOwnerId(), createInput)
if err != nil {
return nil, err
}
if b.RunnerServerId != "" {
if err := b.DeleteRunnerServer(ctx, userCred); err != nil {
return nil, errors.Wrap(err, "delete old benchmark runner")
}
}
if err := b.CleanupArtifacts(ctx); err != nil {
log.Warningf("cleanup benchmark %s artifacts before retest: %s", b.Id, err)
}
_, err = db.Update(b, func() error {
b.LLMId = prepared.LLMId
b.LLMDeploymentId = prepared.LLMDeploymentId
b.LLMSkuId = prepared.LLMSkuId
b.LLMImageId = prepared.LLMImageId
b.BenchmarkPackageId = prepared.BenchmarkPackageId
b.Backend = prepared.Backend
b.TargetUrl = prepared.TargetUrl
b.Model = prepared.Model
b.WorkDir = prepared.WorkDir
b.TargetSnapshot = prepared.TargetSnapshot
b.GuideLLMSpec = prepared.GuideLLMSpec
resetLLMBenchmarkResultFields(b)
return nil
})
if err != nil {
return nil, errors.Wrap(err, "reset benchmark")
}
if err := b.StartRunTask(ctx, userCred, ""); err != nil {
_ = b.SetState(ctx, userCred, api.LLMBenchmarkStateError, err.Error())
return nil, err
}
return nil, nil
}
func requestLLMBenchmarkStop(b *SLLMBenchmark) {
b.StopRequested = true
}
func (b *SLLMBenchmark) PerformStop(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
obj, err := GetLLMBenchmarkManager().FetchById(b.Id)
if err != nil {
return nil, err
}
current := obj.(*SLLMBenchmark)
if !utils.IsInStringArray(current.State, []string{api.LLMBenchmarkStatePending, api.LLMBenchmarkStateQueued, api.LLMBenchmarkStateValidating, api.LLMBenchmarkStateRunning}) {
return nil, httperrors.NewInvalidStatusError("benchmark is %s", current.State)
}
_, err = db.Update(current, func() error {
requestLLMBenchmarkStop(current)
return nil
})
if err != nil {
return nil, err
}
stopLLMBenchmarkAsync(current.Id, userCred)
return nil, nil
}
func stopLLMBenchmarkAsync(benchmarkId string, userCred mcclient.TokenCredential) {
go func() {
ctx := context.Background()
obj, err := GetLLMBenchmarkManager().FetchById(benchmarkId)
if err != nil {
log.Warningf("fetch benchmark %s for stop: %s", benchmarkId, err)
return
}
benchmark := obj.(*SLLMBenchmark)
if err := benchmark.DeleteRunnerServer(ctx, userCred); err != nil {
log.Warningf("delete benchmark %s runner: %s", benchmarkId, err)
}
}()
}
func (b *SLLMBenchmark) DeleteRunnerServer(ctx context.Context, userCred mcclient.TokenCredential) error {
if b.RunnerServerId == "" {
return nil
}
s := auth.GetSession(ctx, userCred, options.Options.Region)
_, _ = compute.Servers.Update(s, b.RunnerServerId, jsonutils.Marshal(map[string]interface{}{
"disable_delete": false,
}))
_, err := compute.Servers.DeleteWithParam(s, b.RunnerServerId, jsonutils.Marshal(map[string]interface{}{
"override_pending_delete": true,
}), nil)
if err != nil && !isBenchmarkRunnerNotFound(err) {
return err
}
_, err = db.Update(b, func() error {
b.RunnerServerId = ""
b.RunnerContainerId = ""
return nil
})
return err
}
func isBenchmarkRunnerNotFound(err error) bool {
var clientErr *httputils.JSONClientError
return stderrors.As(err, &clientErr) && clientErr.Code == 404
}
func (b *SLLMBenchmark) ResolveTokenizerMount() (*LLMBenchmarkTokenizerMount, error) {
obj, err := GetLLMManager().FetchById(b.LLMId)
if err != nil {
return nil, errors.Wrap(err, "fetch benchmark LLM")
}
return resolveBenchmarkTokenizerMount(obj.(*SLLM), b.Model)
}
func (b *SLLMBenchmark) RunnerPodInput(image string, server *computeapi.ServerDetails, tokenizer *LLMBenchmarkTokenizerMount) (*computeapi.ServerCreateInput, error) {
if len(server.Nics) == 0 || server.Nics[0].NetworkId == "" {
return nil, errors.Wrap(httperrors.ErrInvalidStatus, "target server network is empty")
}
cpu := options.Options.LLMBenchmarkRunnerCPU
if cpu <= 0 {
cpu = 1
}
mem := options.Options.LLMBenchmarkRunnerMemoryMB
if mem <= 0 {
mem = 2048
}
input := bench.RunnerPodInput{
Name: "llm-bench-" + b.Id,
Image: image,
NetworkId: server.Nics[0].NetworkId,
HostId: server.HostId,
CPU: cpu,
MemoryMB: mem,
}
if b.BenchmarkPackageId != "" {
obj, err := GetLLMBenchmarkPackageManager().FetchById(b.BenchmarkPackageId)
if err != nil {
return nil, errors.Wrap(err, "fetch benchmark package")
}
pkg := obj.(*SLLMBenchmarkPackage)
input.PackageImageId = pkg.ImageId
input.PackageMountBase = api.LLMBenchmarkPackageMountBase
input.PackageSizeMB = int(pkg.ActualSizeMb) + 512
}
if tokenizer != nil {
input.ModelImageId = tokenizer.ImageId
input.ModelSizeMB = tokenizer.SizeMB
input.ModelMountBase = tokenizer.MountBase
input.ModelMountSubdirectory = tokenizer.MountSubdirectory
}
return bench.BuildRunnerPodInput(input), nil
}