From b0155a0d84e450445221914a4600ecfe523c5315 Mon Sep 17 00:00:00 2001 From: Jonathan Irwin Date: Thu, 2 Apr 2026 14:17:34 -0400 Subject: [PATCH] Add GPU fallback support via unified compute field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compute now accepts either a string or array in the TOML: compute = "HOPPER_H100" compute = ["HOPPER_H100", "HOPPER_H200", "AMPERE_A100_80GB"] First element is primary, rest are fallbacks. CLI normalizes both forms and sends compute as array to the backend API. Changes: - config.go: ComputeRaw (interface{}) for TOML, Compute (*string) unchanged - loader.go: normalizeCompute() splits string/array into Compute + ComputeFallbacks - validator.go: validates primary and fallbacks against compute enum No changes to deploy.go, run.go, or tests — Compute stays *string. Companion backend PR: CerebriumAI/dashboard-backend#3432 Made-with: Cursor --- pkg/projectconfig/config.go | 14 ++++++++++++-- pkg/projectconfig/loader.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/pkg/projectconfig/config.go b/pkg/projectconfig/config.go index bd51d5d..33ec170 100644 --- a/pkg/projectconfig/config.go +++ b/pkg/projectconfig/config.go @@ -28,7 +28,9 @@ type DeploymentConfig struct { type HardwareConfig struct { CPU *float64 `mapstructure:"cpu" toml:"cpu,omitempty"` Memory *float64 `mapstructure:"memory" toml:"memory,omitempty"` - Compute *string `mapstructure:"compute" toml:"compute,omitempty"` + ComputeRaw interface{} `mapstructure:"compute" toml:"compute,omitempty"` + Compute *string `mapstructure:"-" toml:"-"` + ComputeFallbacks []string `mapstructure:"-" toml:"-"` GPUCount *int `mapstructure:"gpu_count" toml:"gpu_count,omitempty"` Provider *string `mapstructure:"provider" toml:"provider,omitempty"` Region *string `mapstructure:"region" toml:"region,omitempty"` @@ -114,7 +116,12 @@ func (pc *ProjectConfig) ToPayload() map[string]any { payload["memory"] = *pc.Hardware.Memory } if pc.Hardware.Compute != nil { - payload["compute"] = *pc.Hardware.Compute + if len(pc.Hardware.ComputeFallbacks) > 0 { + all := append([]string{*pc.Hardware.Compute}, pc.Hardware.ComputeFallbacks...) + payload["compute"] = all + } else { + payload["compute"] = *pc.Hardware.Compute + } } if pc.Hardware.GPUCount != nil && pc.Hardware.Compute != nil && *pc.Hardware.Compute != "CPU" { payload["gpuCount"] = *pc.Hardware.GPUCount @@ -160,6 +167,9 @@ func (pc *ProjectConfig) ToPayload() map[string]any { if pc.Scaling.LoadBalancingAlgorithm != nil { payload["loadBalancingAlgorithm"] = *pc.Scaling.LoadBalancingAlgorithm } + if pc.Scaling.ComputeTier != nil { + payload["computeTier"] = *pc.Scaling.ComputeTier + } // Runtime configuration if pc.CustomRuntime != nil && pc.PartnerService != nil { diff --git a/pkg/projectconfig/loader.go b/pkg/projectconfig/loader.go index 4c18a21..31c915e 100644 --- a/pkg/projectconfig/loader.go +++ b/pkg/projectconfig/loader.go @@ -113,6 +113,11 @@ func Load(configPath string) (*ProjectConfig, error) { } } + // Normalize compute: accepts string or array + if err := normalizeCompute(&config.Hardware); err != nil { + return nil, err + } + // Validate compute_tier before applying defaults if config.Scaling.ComputeTier != nil { tier := *config.Scaling.ComputeTier @@ -127,6 +132,35 @@ func Load(configPath string) (*ProjectConfig, error) { return &config, nil } +func normalizeCompute(hw *HardwareConfig) error { + if hw.ComputeRaw == nil { + return nil + } + switch v := hw.ComputeRaw.(type) { + case string: + hw.Compute = &v + case []interface{}: + if len(v) == 0 { + return fmt.Errorf("compute array must not be empty") + } + first, ok := v[0].(string) + if !ok { + return fmt.Errorf("compute values must be strings") + } + hw.Compute = &first + for _, item := range v[1:] { + s, ok := item.(string) + if !ok { + return fmt.Errorf("compute values must be strings") + } + hw.ComputeFallbacks = append(hw.ComputeFallbacks, s) + } + default: + return fmt.Errorf("compute must be a string or array of strings") + } + return nil +} + // applyDefaults sets default values for fields that weren't specified in the config func applyDefaults(config *ProjectConfig) { // Apply deployment defaults