Skip to content

Commit 21e7c82

Browse files
committed
fix(nnctl): allow CPU cloud deploys
Allow Verda cloud deploy to accept CPU-only spot instance types. Keep the multi-GPU guard in place so large GPU shapes remain blocked. This lets e2e workers use CPU capacity when GPUs are not required. Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
1 parent 520719b commit 21e7c82

5 files changed

Lines changed: 54 additions & 16 deletions

File tree

nnctl/internal/cli/cloud.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ func (a *App) printCloudDeployResult(result *verdacloud.DeployResult, jsonOutput
309309
if location == "" {
310310
location = result.Policy.LocationSelection
311311
}
312-
fmt.Fprintf(a.stdout(), "dry run: would deploy %s as a spot single-GPU instance in %s\n", result.Request.InstanceType, location)
312+
fmt.Fprintf(a.stdout(), "dry run: would deploy %s as a spot instance in %s\n", result.Request.InstanceType, location)
313313
fmt.Fprintf(a.stdout(), "hostname: %s\n", result.Request.Hostname)
314314
fmt.Fprintf(a.stdout(), "source os volume: %s\n", result.SourceOSVolumeID)
315315
if result.Policy.SourceOSVolumeLocked {

nnctl/internal/cli/cloud_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ func TestCloudDeployDryRunJSON(t *testing.T) {
2828
LocationSelection string `json:"location_selection"`
2929
SourceOSVolumeLocked bool `json:"source_os_volume_locked"`
3030
SpotOnly bool `json:"spot_only"`
31-
SingleGPU bool `json:"single_gpu"`
31+
AllowsCPU bool `json:"allows_cpu"`
32+
MaxGPUCount int `json:"max_gpu_count"`
3233
} `json:"policy"`
3334
SourceOSVolumeID string `json:"source_os_volume_id"`
3435
Request struct {
@@ -51,7 +52,7 @@ func TestCloudDeployDryRunJSON(t *testing.T) {
5152
if result.SourceOSVolumeID != "vol-golden" {
5253
t.Fatalf("SourceOSVolumeID = %q", result.SourceOSVolumeID)
5354
}
54-
if result.Policy.LocationCode != "" || result.Policy.LocationSelection != "source_os_volume_location" || !result.Policy.SourceOSVolumeLocked || !result.Policy.SpotOnly || !result.Policy.SingleGPU {
55+
if result.Policy.LocationCode != "" || result.Policy.LocationSelection != "source_os_volume_location" || !result.Policy.SourceOSVolumeLocked || !result.Policy.SpotOnly || !result.Policy.AllowsCPU || result.Policy.MaxGPUCount != 1 {
5556
t.Fatalf("policy was not encoded: %#v", result.Policy)
5657
}
5758
}

nnctl/internal/cli/cobra.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,8 @@ changes, so remote benchmark runs can be tied back to a specific git ref.`,
234234
func (a *App) newCloudCommand() *cobra.Command {
235235
cmd := &cobra.Command{
236236
Use: "cloud <command>",
237-
Short: "Deploy cloud GPU benchmark workers",
238-
Long: "Deploys cloud GPU benchmark workers for nnctl agents. Verda deployments are restricted to spot, single-GPU instances.",
237+
Short: "Deploy cloud benchmark workers",
238+
Long: "Deploys cloud benchmark workers for nnctl agents. Verda deployments are restricted to spot CPU-only or single-GPU instances.",
239239
RunE: func(cmd *cobra.Command, args []string) error {
240240
return cmd.Help()
241241
},
@@ -257,10 +257,10 @@ func (a *App) newCloudDeployCommand() *cobra.Command {
257257
}
258258
cmd := &cobra.Command{
259259
Use: "deploy",
260-
Short: "Deploy a Verda spot GPU worker",
261-
Long: `Deploys one Verda GPU instance for nnctl benchmark work.
262-
The deployment policy is intentionally narrow: spot only and single-GPU instance
263-
types only. By default nnctl chooses the cheapest currently available spot
260+
Short: "Deploy a Verda spot benchmark worker",
261+
Long: `Deploys one Verda spot instance for nnctl benchmark work.
262+
The deployment policy is intentionally narrow: spot only, with CPU-only and
263+
single-GPU instance types accepted. By default nnctl uses the source OS volume
264264
location for the requested instance type. Userdata defaults to the embedded
265265
script from nnctl/internal/cloud/verda/bootstrap.sh.`,
266266
Example: ` nnctl cloud deploy --instance-type 1V100.6V --source-os-volume-id volume_id --ssh-key-id ssh_key_id
@@ -279,7 +279,7 @@ script from nnctl/internal/cloud/verda/bootstrap.sh.`,
279279
cmd.Flags().StringVar(&opts.Hostname, "hostname", opts.Hostname, "instance hostname")
280280
cmd.Flags().StringVar(&opts.Description, "description", opts.Description, "instance description")
281281
cmd.Flags().StringArrayVar(&opts.SSHKeyIDs, "ssh-key-id", opts.SSHKeyIDs, "Verda SSH key ID to attach (repeatable)")
282-
cmd.Flags().StringVar(&opts.LocationCode, "location-code", opts.LocationCode, "Verda location code; defaults to cheapest currently available spot location")
282+
cmd.Flags().StringVar(&opts.LocationCode, "location-code", opts.LocationCode, "Verda location code; defaults to source OS volume location")
283283
cmd.Flags().StringVar(&opts.StartupScriptName, "startup-script-name", opts.StartupScriptName, "Verda startup script name")
284284
cmd.Flags().StringVar(&opts.userDataFile, "user-data-file", opts.userDataFile, "read userdata from a file instead of the embedded script")
285285
cmd.Flags().StringVar(&opts.BaseURL, "base-url", opts.BaseURL, "Verda API base URL")

nnctl/internal/cloud/verda/deploy.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ const (
1717
ExplicitLocation = "explicit"
1818
SourceOSVolumeLocation = "source_os_volume_location"
1919
SpotContract = "SPOT"
20-
DefaultDescription = "nnctl GPU benchmark worker"
20+
DefaultDescription = "nnctl benchmark worker"
21+
MaxDeployGPUCount = 1
2122
VolumeReadyTimeout = 20 * time.Minute
2223
VolumeReadyPoll = 5 * time.Second
2324
)
@@ -131,6 +132,8 @@ type DeployPolicy struct {
131132
SourceOSVolumeLocked bool `json:"source_os_volume_locked"`
132133
SpotOnly bool `json:"spot_only"`
133134
SingleGPU bool `json:"single_gpu"`
135+
AllowsCPU bool `json:"allows_cpu"`
136+
MaxGPUCount int `json:"max_gpu_count"`
134137
CleanupClonedOSVolumeOnFailure bool `json:"cleanup_cloned_os_volume_on_failure"`
135138
}
136139

@@ -182,6 +185,8 @@ func Deploy(ctx context.Context, client Client, opts DeployOptions) (*DeployResu
182185
SourceOSVolumeLocked: true,
183186
SpotOnly: true,
184187
SingleGPU: true,
188+
AllowsCPU: true,
189+
MaxGPUCount: MaxDeployGPUCount,
185190
CleanupClonedOSVolumeOnFailure: !normalized.KeepClonedOSVolumeOnFailure,
186191
},
187192
SourceOSVolumeID: normalized.SourceOSVolumeID,
@@ -219,8 +224,8 @@ func Deploy(ctx context.Context, client Client, opts DeployOptions) (*DeployResu
219224
if err != nil {
220225
return nil, fmt.Errorf("get Verda instance type %q: %w", normalized.InstanceType, err)
221226
}
222-
if instanceType.GPUCount != 1 {
223-
return nil, fmt.Errorf("instance type %q has %d GPUs; nnctl cloud deploy only accepts single-GPU instance types", normalized.InstanceType, instanceType.GPUCount)
227+
if instanceType.GPUCount > MaxDeployGPUCount {
228+
return nil, fmt.Errorf("instance type %q has %d GPUs; nnctl cloud deploy accepts CPU-only and single-GPU instance types", normalized.InstanceType, instanceType.GPUCount)
224229
}
225230
result.InstanceType = &instanceType
226231

nnctl/internal/cloud/verda/deploy_test.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func TestDeployDryRunBuildsSpotAutoPlacementRequestWithoutClient(t *testing.T) {
5454
if !result.DryRun {
5555
t.Fatal("DryRun = false")
5656
}
57-
if result.Policy.LocationCode != "" || result.Policy.LocationSelection != SourceOSVolumeLocation || !result.Policy.SourceOSVolumeLocked || !result.Policy.SpotOnly || !result.Policy.SingleGPU || !result.Policy.CleanupClonedOSVolumeOnFailure {
57+
if result.Policy.LocationCode != "" || result.Policy.LocationSelection != SourceOSVolumeLocation || !result.Policy.SourceOSVolumeLocked || !result.Policy.SpotOnly || !result.Policy.AllowsCPU || result.Policy.MaxGPUCount != MaxDeployGPUCount || !result.Policy.CleanupClonedOSVolumeOnFailure {
5858
t.Fatalf("unexpected policy: %#v", result.Policy)
5959
}
6060
if !result.Request.IsSpot || result.Request.Contract != SpotContract || result.Request.LocationCode != "" {
@@ -80,14 +80,46 @@ func TestDeployRejectsMultiGPUInstanceTypes(t *testing.T) {
8080
if err == nil {
8181
t.Fatal("expected multi-GPU instance type error")
8282
}
83-
if !strings.Contains(err.Error(), "single-GPU") {
84-
t.Fatalf("error did not mention single-GPU policy: %v", err)
83+
if !strings.Contains(err.Error(), "CPU-only and single-GPU") {
84+
t.Fatalf("error did not mention CPU/single-GPU policy: %v", err)
8585
}
8686
if client.createdScript {
8787
t.Fatal("startup script should not be created for rejected instance type")
8888
}
8989
}
9090

91+
func TestDeployCreatesSpotCPUInstanceInSourceOSVolumeLocation(t *testing.T) {
92+
client := &fakeClient{
93+
sourceVolume: Volume{ID: "vol-golden", Name: "golden", Status: "detached", Location: FinlandLocationCode, IsOSVolume: true},
94+
instanceType: InstanceType{InstanceType: "4C.16M", GPUCount: 0},
95+
placements: []SpotPlacement{
96+
{LocationCode: FinlandLocationCode, SpotPrice: 1, PriceKnown: true, Currency: "eur"},
97+
},
98+
script: StartupScript{ID: "script-1", Name: "userdata"},
99+
clonedVolume: Volume{ID: "vol-clone-1", Name: "worker-os", Status: "cloning", Location: FinlandLocationCode},
100+
readyVolume: Volume{ID: "vol-clone-1", Name: "worker-os", Status: "detached", Location: FinlandLocationCode},
101+
instance: Instance{ID: "inst-1", Hostname: "worker", Status: "new", InstanceType: "4C.16M", Location: FinlandLocationCode, IsSpot: true},
102+
}
103+
opts := DefaultDeployOptions("4C.16M")
104+
opts.SourceOSVolumeID = "vol-golden"
105+
opts.Hostname = "worker"
106+
107+
result, err := Deploy(context.Background(), client, opts)
108+
if err != nil {
109+
t.Fatal(err)
110+
}
111+
112+
if result.InstanceType == nil || result.InstanceType.GPUCount != 0 {
113+
t.Fatalf("unexpected instance type result: %#v", result.InstanceType)
114+
}
115+
if result.Policy.MaxGPUCount != MaxDeployGPUCount || !result.Policy.AllowsCPU {
116+
t.Fatalf("unexpected policy: %#v", result.Policy)
117+
}
118+
if client.createRequest.InstanceType != "4C.16M" || client.createRequest.LocationCode != FinlandLocationCode {
119+
t.Fatalf("unexpected create request: %#v", client.createRequest)
120+
}
121+
}
122+
91123
func TestDeployCreatesSpotInstanceInSourceOSVolumeLocation(t *testing.T) {
92124
client := &fakeClient{
93125
sourceVolume: Volume{ID: "vol-golden", Name: "golden", Status: "detached", Location: FinlandLocationCode, IsOSVolume: true},

0 commit comments

Comments
 (0)