Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/runware_serverless_apps_scale.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ runware serverless apps scale <appId> [flags]
--available-workers-pct int32 Idle-worker buffer as a percentage of load (0-100)
--fallback-gpu-type string Secondary GPU type if the preferred type is unavailable
--gpu-type string Preferred GPU type ID (see 'serverless gpus')
--gpus-per-worker int32 GPUs allocated per worker
--gpus-per-worker int32 GPUs allocated per worker (1, 2, 4, or 8)
-h, --help help for scale
--idle-ttl int32 Idle TTL in seconds before scaling down
--max-workers int32 Maximum number of workers
Expand Down
2 changes: 1 addition & 1 deletion docs/runware_serverless_deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ runware serverless deploy [file] [flags]
--env stringArray Environment variable as KEY=VALUE (repeatable)
--env-file stringArray File of KEY=VALUE lines to read environment variables from (repeatable)
--gpu-type string GPU type ID (see 'serverless gpus'; required when creating)
--gpus-per-worker int32 GPUs allocated per worker (default 1)
--gpus-per-worker int32 GPUs allocated per worker (1, 2, 4, or 8) (default 1)
-h, --help help for deploy
--id string Application ID (immutable, lowercase slug)
--idle-ttl int32 Idle TTL in seconds before scaling down (default 60)
Expand Down
5 changes: 4 additions & 1 deletion internal/cmd/serverless/apps_scale.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func bindScaleFlags(cmd *cobra.Command, flags *scaleFlags) {
f.Int32Var(&flags.idleTTL, "idle-ttl", 0, "Idle TTL in seconds before scaling down")
f.Int32Var(&flags.scalingDelay, "scaling-delay", 0, "Scaling delay in seconds")
f.StringVar(&flags.gpuType, "gpu-type", "", "Preferred GPU type ID (see 'serverless gpus')")
f.Int32Var(&flags.gpusPerWorker, "gpus-per-worker", 0, "GPUs allocated per worker")
f.Int32Var(&flags.gpusPerWorker, "gpus-per-worker", 0, "GPUs allocated per worker ("+gpusPerWorkerValuesText()+")")
f.StringVar(&flags.fallbackGPUType, "fallback-gpu-type", "", "Secondary GPU type if the preferred type is unavailable")
f.Int32Var(&flags.minAvailableWorkers, "min-available-workers", 0, "Minimum idle workers kept as a buffer")
f.Int32Var(&flags.availableWorkersPct, "available-workers-pct", 0, "Idle-worker buffer as a percentage of load (0-100)")
Expand All @@ -97,6 +97,9 @@ func bindScaleFlags(cmd *cobra.Command, flags *scaleFlags) {
// were explicitly set. Unchanged flags are omitted so existing values are
// not cleared.
func workerConfigPatchFromFlags(cmd *cobra.Command, flags scaleFlags) (*serverlessapi.WorkerConfigPatch, error) {
if err := validateGPUsPerWorkerFlag(cmd, flags.gpusPerWorker); err != nil {
return nil, err
}
patch := &serverlessapi.WorkerConfigPatch{
MaxWorkers: optionalInt32Ptr(cmd, "max-workers", flags.maxWorkers),
MinWorkers: optionalInt32Ptr(cmd, "min-workers", flags.minWorkers),
Expand Down
11 changes: 11 additions & 0 deletions internal/cmd/serverless/apps_scale_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ func TestWorkerConfigPatchFromFlags_EachFlag(t *testing.T) {
}
}

func TestWorkerConfigPatchFromFlags_RejectsGPUsPerWorker(t *testing.T) {
cmd, flags := newScaleFlagCmd()
if err := cmd.ParseFlags([]string{"--gpus-per-worker", "3"}); err != nil {
t.Fatalf("ParseFlags: %v", err)
}
_, err := workerConfigPatchFromFlags(cmd, *flags)
if err == nil || !strings.Contains(err.Error(), gpusPerWorkerValuesText()) {
t.Fatalf("workerConfigPatchFromFlags = %v, want an allowed-values error", err)
}
}

func TestWorkerConfigPatchFromFlags_RequiresAFlag(t *testing.T) {
cmd, flags := newScaleFlagCmd()
if err := cmd.ParseFlags([]string{}); err != nil {
Expand Down
43 changes: 42 additions & 1 deletion internal/cmd/serverless/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log/slog"
"strings"
"time"

"github.com/charmbracelet/log"
Expand Down Expand Up @@ -228,6 +229,9 @@ paths and an invoke example once the application is active.`,
if err := validateDeployArgs(cmd, args, containerDir); err != nil {
return err
}
if err := validateGPUsPerWorkerFlag(cmd, gpusPerWorker); err != nil {
return err
}
if name == "" {
name = id
}
Expand Down Expand Up @@ -388,7 +392,7 @@ paths and an invoke example once the application is active.`,
cmd.Flags().StringVar(&gpuType, "gpu-type", "", "GPU type ID (see 'serverless gpus'; required when creating)")
cmd.Flags().StringArrayVar(&requirements, "requirement", nil, "Additional pip package to install (repeatable; code deploys only)")
cmd.Flags().Int32Var(&minWorkers, "min-workers", 0, "Minimum number of workers")
cmd.Flags().Int32Var(&gpusPerWorker, "gpus-per-worker", 1, "GPUs allocated per worker")
cmd.Flags().Int32Var(&gpusPerWorker, "gpus-per-worker", 1, "GPUs allocated per worker ("+gpusPerWorkerValuesText()+")")
cmd.Flags().BoolVar(&wait, "wait", false, "Poll until the application is active or failed")
cmd.Flags().DurationVar(&pollInterval, "poll-interval", 2*time.Second, "Polling interval when waiting for the application")

Expand Down Expand Up @@ -417,6 +421,43 @@ func validateCreateDeployGPU(gpuType string) error {
return nil
}

// gpusPerWorkerAllowed are the group sizes create and scale accept, in
// display order. The API rejects anything else with 422, which on deploy
// is after the archive upload.
var gpusPerWorkerAllowed = []int32{
1,
2,
4,
8,
}

func gpusPerWorkerValuesText() string {
parts := make([]string, len(gpusPerWorkerAllowed))
for i, n := range gpusPerWorkerAllowed {
parts[i] = fmt.Sprint(n)
}
if len(parts) == 1 {
return parts[0]
}
return strings.Join(parts[:len(parts)-1], ", ") + ", or " + parts[len(parts)-1]
}

func validateGPUsPerWorker(n int32) error {
for _, allowed := range gpusPerWorkerAllowed {
if n == allowed {
return nil
}
}
return fmt.Errorf("--gpus-per-worker must be %s", gpusPerWorkerValuesText())
}

func validateGPUsPerWorkerFlag(cmd *cobra.Command, n int32) error {
if !cmd.Flags().Changed("gpus-per-worker") {
return nil
}
return validateGPUsPerWorker(n)
}

func validateUpdateDeployFlags(cmd *cobra.Command) error {
for _, name := range createOnlyDeployFlags {
if cmd.Flags().Changed(name) {
Expand Down
26 changes: 26 additions & 0 deletions internal/cmd/serverless/deploy_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package serverless

import (
"bytes"
"context"
"encoding/json"
"log/slog"
Expand Down Expand Up @@ -254,6 +255,31 @@ func TestExistingApp(t *testing.T) {
}
}

func TestValidateGPUsPerWorker(t *testing.T) {
Comment thread
Ryank90 marked this conversation as resolved.
for _, n := range []int32{1, 2, 4, 8} {
if err := validateGPUsPerWorker(n); err != nil {
t.Errorf("validateGPUsPerWorker(%d): %v", n, err)
}
}
for _, n := range []int32{0, 3, 5, 16} {
err := validateGPUsPerWorker(n)
if err == nil || !strings.Contains(err.Error(), gpusPerWorkerValuesText()) {
t.Errorf("validateGPUsPerWorker(%d) = %v, want an allowed-values error", n, err)
}
}
}

func TestDeploy_RejectsInvalidGPUsPerWorkerBeforeUpload(t *testing.T) {
cmd := newDeployCmd(nil)
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs([]string{testModelFile, "--id", testAppID, "--gpus-per-worker", "3"})
err := cmd.Execute()
if err == nil || !strings.Contains(err.Error(), gpusPerWorkerValuesText()) {
t.Fatalf("err = %v", err)
}
}

func TestValidateCreateDeployGPU(t *testing.T) {
if err := validateCreateDeployGPU(""); err == nil || !strings.Contains(err.Error(), "--gpu-type") {
t.Fatalf("empty: %v", err)
Expand Down
Loading