Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,6 @@ spec:
{{- with .Values.controllerManager.manager.dgdrDefaultImage }}
- {{ printf "--dgdr-default-image=%s" . | quote }}
{{- end }}
{{- with .Values.controllerManager.manager.dgdrAISimulateDefaultImage }}
- {{ printf "--dgdr-aisimulate-default-image=%s" . | quote }}
{{- end }}
{{- with .Values.controllerManager.manager.dgdrAISimulateDefaultVersionOverride }}
- {{ printf "--dgdr-aisimulate-default-version-override=%s" . | quote }}
{{- end }}
command:
- /manager
env:
Expand Down
10 changes: 0 additions & 10 deletions deploy/helm/charts/platform/components/operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,6 @@ controllerManager:
# derives dynamo-planner:<chart appVersion>, which only exists for GA
# releases.
dgdrDefaultImage: ""
# AISimulate runner image put into v1beta2 DGDR spec.aisimulate.image when unset.
# Empty derives nvcr.io/nvidia/ai-dynamo/dynamo-planner:<operator-version>.
dgdrAISimulateDefaultImage: ""
# AISimulate version bundled in the default image (for example 0.13.0), put into v1beta2 DGDR
# spec.aisimulate.versionOverride together with that image. Empty uses the AISimulate version
# the operator maps to the image's Dynamo release. Required when the image is not a mapped
# Dynamo release (for example nightly); the operator refuses to start without it. It also
# applies to the derived planner image when dgdrAISimulateDefaultImage is empty. A Request
# that names its own image never receives it.
dgdrAISimulateDefaultVersionOverride: ""
resources:
limits:
cpu: 1024m
Expand Down
33 changes: 16 additions & 17 deletions deploy/operator/api/roundtrip_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,22 +568,21 @@ func fuzzBeta2Workload(workload *v1beta2.WorkloadSpec, c randfill.Continue) {
}

static := workload.Static
static.ISL = oneOfPtr(c, int32(1), int32(4000), int32(131072))
static.OSL = oneOfPtr(c, int32(1), int32(1000), int32(16384))
static.NumRequestRatio = oneOfPtr(c, 0.5, 1.0, 1000.0)
static.RequestRate = nil
static.Concurrency = nil
static.KVLoadRatio = nil
static.InputTokens = oneOfPtr(c, int32(1), int32(4000), int32(131072))
static.OutputTokens = oneOfPtr(c, int32(1), int32(1000), int32(16384))
static.Load = nil
switch c.Intn(4) {
case 1:
static.RequestRate = oneOfPtr(c, 0.1, 1.0, 100.0)
static.Load = &v1beta2.StaticLoadSpec{RequestsPerSecond: oneOfPtr(c, 0.1, 1.0, 100.0)}
case 2:
static.Concurrency = oneOfPtr(c, int32(1), int32(32), int32(1024))
static.Load = &v1beta2.StaticLoadSpec{Concurrency: oneOfPtr(c, int32(1), int32(32), int32(1024))}
case 3:
if c.Bool() {
static.KVLoadRatio = v1beta2.KVLoadRatio{0.5}
static.Load = &v1beta2.StaticLoadSpec{KVCapacityFraction: &v1beta2.KVCapacityFractionSpec{Value: new(0.5)}}
} else {
static.KVLoadRatio = v1beta2.KVLoadRatio{0.0, 1.0}
static.Load = &v1beta2.StaticLoadSpec{KVCapacityFraction: &v1beta2.KVCapacityFractionSpec{
Range: &v1beta2.KVCapacityFractionRange{Min: 0.1, Max: 1.0},
}}
}
}
}
Expand Down Expand Up @@ -653,7 +652,7 @@ func isGoodputMetric(metric v1beta2.ObjectiveMetric) bool {
return metric == "Goodput" || metric == "GoodputPerGPU"
}

type fuzzAISimulateParameters struct {
type fuzzAISimulateInputOverrides struct {
Search *fuzzAISimulateSearch `json:"search,omitempty"`
Workload *fuzzAISimulateWorkload `json:"workload,omitempty"`
}
Expand All @@ -671,20 +670,20 @@ type fuzzAISimulateWorkload struct {
func fuzzAISimulateSpec(s *v1beta2.AISimulateSpec, c randfill.Continue) {
c.FillNoCustom(s)

// Exercise both absent and typed object parameter payloads.
// Exercise both absent and typed object input override payloads.
if c.Bool() {
s.Parameters = nil
s.InputOverrides = nil
return
}

// Marshal a typed test input instead of generating arbitrary RawExtension bytes.
parameters := &fuzzAISimulateParameters{}
c.Fill(parameters)
raw, err := json.Marshal(parameters)
overrides := &fuzzAISimulateInputOverrides{}
c.Fill(overrides)
raw, err := json.Marshal(overrides)
if err != nil {
panic(err)
}
s.Parameters = &runtime.RawExtension{Raw: raw}
s.InputOverrides = &runtime.RawExtension{Raw: raw}
}

func marshalFuzzEnvelope(value any) *apiextensionsv1.JSON {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,9 @@ func TestV1Beta2RoundTripUsesEnvelope(t *testing.T) {
},
Workload: &WorkloadSpec{
Static: &StaticWorkloadSpec{
ISL: ptr.To(int32(4096)),
OSL: ptr.To(int32(1024)),
NumRequestRatio: ptr.To(2.0),
RequestRate: ptr.To(12.5),
InputTokens: ptr.To(int32(4096)),
OutputTokens: ptr.To(int32(1024)),
Load: &StaticLoadSpec{RequestsPerSecond: ptr.To(12.5)},
},
},
Objective: &ObjectiveSpec{
Expand All @@ -113,9 +112,7 @@ func TestV1Beta2RoundTripUsesEnvelope(t *testing.T) {
SLA: &ObjectiveSLASpec{TTFTMs: ptr.To(2000.0), ITLMs: ptr.To(30.0)},
},
AISimulate: &AISimulateSpec{
Image: "nvcr.io/nvidia/aisimulate:latest",
VersionOverride: "1.0.0",
Parameters: &runtime.RawExtension{Raw: []byte(`{"search_method":"grid"}`)},
InputOverrides: &runtime.RawExtension{Raw: []byte(`{"search_method":"grid"}`)},
},
Recommendation: &RecommendationSpec{MaxCandidates: 3},
},
Expand Down
174 changes: 95 additions & 79 deletions deploy/operator/api/v1beta2/dynamographdeploymentrequest_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,67 +64,94 @@ type HardwareSpec struct {
GPU *GPUHardwareSpec `json:"gpu,omitempty"`
}

// KVLoadRatio is either a pinned load ratio or a minimum/maximum range.
// AISimulate performs mode-specific validation.
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=2
// +kubebuilder:validation:items:Minimum=0
// +kubebuilder:validation:items:Maximum=1
// +kubebuilder:validation:XValidation:rule="size(self) != 2 || self[0] < self[1]",message="a range needs min < max"
type KVLoadRatio []float64

// StaticWorkloadSpec defines one of AISimulate's synthetic workload shapes.
// +kubebuilder:validation:XValidation:rule="(has(self.requestRate) ? 1 : 0) + (has(self.concurrency) ? 1 : 0) + (has(self.kvLoadRatio) ? 1 : 0) <= 1",message="at most one of requestRate, concurrency, or kvLoadRatio may be specified"
type StaticWorkloadSpec struct {
// ISL is the input sequence length in tokens.
// +optional
// +kubebuilder:validation:Minimum=1
ISL *int32 `json:"isl,omitempty"`
// KVCapacityFractionRange defines the searched fraction of a candidate's KV capacity.
// +kubebuilder:validation:XValidation:rule="self.min < self.max",message="min must be less than max"
type KVCapacityFractionRange struct {
// Min is the lower end of the searched fraction range.
// +kubebuilder:validation:ExclusiveMinimum=true
// +kubebuilder:validation:Minimum=0
// +kubebuilder:validation:Maximum=1
Min float64 `json:"min"`

// OSL is the output sequence length in tokens.
// +optional
// +kubebuilder:validation:Minimum=1
OSL *int32 `json:"osl,omitempty"`
// Max is the upper end of the searched fraction range.
// +kubebuilder:validation:ExclusiveMinimum=true
// +kubebuilder:validation:Minimum=0
// +kubebuilder:validation:Maximum=1
Max float64 `json:"max"`
}

// NumRequestRatio sets request count relative to the selected load.
// KVCapacityFractionSpec selects either one fixed fraction or a searched range.
// +kubebuilder:validation:XValidation:rule="(has(self.value) ? 1 : 0) + (has(self.range) ? 1 : 0) == 1",message="exactly one of value or range must be specified"
type KVCapacityFractionSpec struct {
// Value uses a fixed fraction of the candidate's KV capacity. A value of 1
// uses its full estimated capacity.
// +optional
// +kubebuilder:validation:ExclusiveMinimum=true
// +kubebuilder:validation:Minimum=0
NumRequestRatio *float64 `json:"numRequestRatio,omitempty"`
// +kubebuilder:validation:Maximum=1
Value *float64 `json:"value,omitempty"`

// Range searches fractions between min and max.
// +optional
Range *KVCapacityFractionRange `json:"range,omitempty"`
}

// RequestRate is a fixed open-loop arrival rate in requests per second.
// StaticLoadSpec selects exactly one synthetic load model by field presence.
// +kubebuilder:validation:XValidation:rule="(has(self.requestsPerSecond) ? 1 : 0) + (has(self.concurrency) ? 1 : 0) + (has(self.kvCapacityFraction) ? 1 : 0) == 1",message="exactly one of requestsPerSecond, concurrency, or kvCapacityFraction must be specified"
type StaticLoadSpec struct {
// RequestsPerSecond is a fixed open-loop arrival rate.
// +optional
// +kubebuilder:validation:ExclusiveMinimum=true
// +kubebuilder:validation:Minimum=0
RequestRate *float64 `json:"requestRate,omitempty"`
RequestsPerSecond *float64 `json:"requestsPerSecond,omitempty"`

// Concurrency is a fixed closed-loop in-flight request cap.
// +optional
// +kubebuilder:validation:Minimum=1
Concurrency *int32 `json:"concurrency,omitempty"`

// KVLoadRatio derives concurrency from candidate KV capacity. A two-value
// range requires a bounded AISimulate sweep.
// KVCapacityFraction derives concurrency from each candidate's estimated KV capacity.
// +optional
KVLoadRatio KVLoadRatio `json:"kvLoadRatio,omitempty"`
KVCapacityFraction *KVCapacityFractionSpec `json:"kvCapacityFraction,omitempty"`
}

// StaticWorkloadSpec defines AISimulate synthetic traffic. Admission completes a nonempty
// object with the omitted static defaults; an explicit empty object opts out of them.
type StaticWorkloadSpec struct {
// InputTokens is the synthetic request input length in tokens.
// +optional
// +kubebuilder:validation:Minimum=1
InputTokens *int32 `json:"inputTokens,omitempty"`

// OutputTokens is the synthetic request output length in tokens.
// +optional
// +kubebuilder:validation:Minimum=1
OutputTokens *int32 `json:"outputTokens,omitempty"`

// Load selects the synthetic load model. When omitted from a nonempty static
// workload, admission defaults it to concurrency 10.
// +optional
Load *StaticLoadSpec `json:"load,omitempty"`
}

// WorkloadSpec defines the synthetic traffic model used for every candidate evaluation.
type WorkloadSpec struct {
// Static defines one of AISimulate's synthetic workload shapes. Its fields are not
// defaulted individually: the default belongs to spec.workload as a whole, so an
// explicit workload object keeps exactly the fields it names.
// Static defines AISimulate synthetic traffic. When omitted, admission supplies the
// complete static default. An explicit empty object opts out; any nonempty object is
// completed with omitted inputTokens, outputTokens and load defaults.
// +optional
Static *StaticWorkloadSpec `json:"static,omitempty"`
}

// ObjectiveMode selects scalar optimization or a multi-objective Pareto search.
// +kubebuilder:validation:Enum=Optimize;Pareto
// ObjectiveMode selects the optimization mode. Pareto is reserved for a future
// release and is not admitted until Candidate publication supports Pareto fronts.
// +kubebuilder:validation:Enum=Optimize
type ObjectiveMode string

const (
ObjectiveModeOptimize ObjectiveMode = "Optimize"
ObjectiveModePareto ObjectiveMode = "Pareto"
// ObjectiveModePareto is reserved for the future Pareto API implementation.
ObjectiveModePareto ObjectiveMode = "Pareto"
)

// ObjectiveMetric is a metric optimized by AISimulate.
Expand Down Expand Up @@ -154,68 +181,57 @@ type ObjectiveSLASpec struct {
E2EMs *float64 `json:"e2eMs,omitempty"`
}

// ObjectiveSpec defines scalar optimization or a multi-objective Pareto search.
// +kubebuilder:validation:XValidation:rule="!has(self.mode) || (self.mode == 'Optimize' && !has(self.metrics)) || (self.mode == 'Pareto' && !has(self.metric) && has(self.metrics))",message="Optimize accepts only metric; Pareto requires metrics"
// ObjectiveSpec defines scalar optimization. Pareto fields are reserved for a future release.
// +kubebuilder:validation:XValidation:rule="!has(self.metrics)",message="metrics is reserved for the future Pareto implementation"
// +kubebuilder:validation:XValidation:rule="!(has(self.metric) && self.metric in ['Goodput', 'GoodputPerGPU']) || (has(self.sla) && (has(self.sla.ttftMs) || has(self.sla.itlMs) || has(self.sla.e2eMs)))",message="Goodput and GoodputPerGPU require at least one SLA bound"
// +kubebuilder:validation:XValidation:rule="!(has(self.metrics) && self.metrics.exists(m, m in ['Goodput', 'GoodputPerGPU'])) || (has(self.sla) && (has(self.sla.ttftMs) || has(self.sla.itlMs) || has(self.sla.e2eMs)))",message="Pareto metrics containing Goodput or GoodputPerGPU require at least one SLA bound"
type ObjectiveSpec struct {
// Mode selects scalar optimization or a Pareto search.
// Mode selects scalar optimization. Only Optimize is currently supported.
// +optional
Mode ObjectiveMode `json:"mode,omitempty"`

// Metric is the scalar target when mode is Optimize.
// When omitted in Optimize mode, admission defaults it to Throughput.
// When omitted in Optimize mode, admission defaults it to ThroughputPerGPU.
// +optional
Metric *ObjectiveMetric `json:"metric,omitempty"`

// Metrics lists two or more distinct scalar targets when mode is Pareto.
// Metrics is reserved for the future Pareto implementation.
// +optional
// +kubebuilder:validation:MinItems=2
// +kubebuilder:validation:MaxItems=7
// +kubebuilder:validation:MaxItems=2
// +listType=set
Metrics []ObjectiveMetric `json:"metrics,omitempty"`

// SLA defines per-request latency bounds used to calculate goodput. When omitted, no
// bounds apply. spec.aisimulate.parameters.goal.sla overrides the bounds it names.
// SLA defines per-request latency bounds used to calculate goodput. Admission defaults an
// omitted SLA to TTFT 2000 ms and ITL 30 ms. An explicit empty object opts out of those
// defaults. spec.aisimulate.inputOverrides.evaluation.sla overrides the bounds it names.
// +optional
SLA *ObjectiveSLASpec `json:"sla,omitempty"`
}

// AISimulateSpec selects the AISimulate execution artifact and supplies native input.
// AISimulateSpec supplies public recommendation input to the AISimulate version in the operator's configured
// planner image.
type AISimulateSpec struct {
// Image selects a Dynamo-compatible AISimulate runner. When omitted, admission
// persists the operator's configured AISimulate image, or the Dynamo planner image of
// the operator's release, together with the AISimulate version it bundles.
// +optional
// +kubebuilder:validation:MinLength=1
Image string `json:"image,omitempty"`

// VersionOverride declares the AISimulate version the image bundles as three numeric parts

// (for example 0.13.0), not a semver version: no v prefix, pre-release or build suffix.

// When omitted, the version is the one the operator maps to the Dynamo release in the

// image tag. It is required when the tag is not a Dynamo release the operator maps (a

// nightly or custom image), and it wins over the mapping.
// +optional
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:Pattern=`^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$`
VersionOverride string `json:"versionOverride,omitempty"`

// Parameters contains native AISimulate input. Field names and nesting use
// the upstream AISimulate JSON contract, including snake_case names.
// InputOverrides is an RFC 7386 JSON Merge Patch over the input lowered from
// the typed fields. It uses the upstream AISimulate JSON contract, including
// snake_case names. Values here take precedence, and null deletes a lowered
// field. The dgdr_run member must remain an object, and its operator-owned
// transport, identity, and runtime fields name, max_candidates, snapshot_dir,
// runtime_image, runtime_version_override, derive_runtime_images,
// num_gpus_per_node, and namespace are operator-owned. Request admission
// rejects them, and the Run controller enforces its values while lowering the
// immutable snapshot. The operator runs the AISimulate version from its
// configured planner image; Requests and Runs cannot select a different image
// or version.
// +optional
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Type=object
Parameters *runtime.RawExtension `json:"parameters,omitempty"`
InputOverrides *runtime.RawExtension `json:"inputOverrides,omitempty"`
}

// RecommendationSpec controls bounded projection into DGDC resources.
type RecommendationSpec struct {
// MaxCandidates is the maximum number of DGDCs owned by one run. Scalar
// candidates are ordered best first; Pareto candidates have no scalar rank.
// candidates are ordered best first.
// This field does not limit internal trials.
// +optional
// +kubebuilder:validation:Minimum=1
Expand All @@ -242,23 +258,23 @@ type NativeSearchInput struct {
Hardware *HardwareSpec `json:"hardware,omitempty"`

// Workload defines the synthetic traffic model used for every candidate evaluation.
// When omitted, admission supplies a static shape of ISL 1024, OSL 128 and concurrency 10;
// an explicit empty workload receives the same shape from admission. An explicit
// workload.static: {} opts out of that shape, leaving the workload to
// spec.aisimulate.parameters.workload. spec.aisimulate.parameters.workload overrides the
// fields it names.
// The default evaluates a fixed concurrency, so it creates neither sweep.max_trials nor a
// KV-load ratio search.
// When omitted, admission supplies 1024 input tokens, 128 output tokens and concurrency 10;
// an explicit empty workload receives the same shape. workload.static: {} opts out of all
// traffic defaults. Any other static object is completed with its missing defaults.
// spec.aisimulate.inputOverrides is applied as an RFC 7386 JSON Merge Patch and wins over
// every lowered traffic field it names.
// The default evaluates a fixed concurrency, so it does not create a KV-capacity-fraction
// search. Lowering independently defaults optimizer.max_trials to 320.
// +optional
Workload *WorkloadSpec `json:"workload,omitempty"`

// Objective defines scalar optimization or a multi-objective Pareto search.
// When omitted, admission supplies the object. Its mode defaults to Optimize, and admission
// supplies the conditional scalar metric.
// Objective defines scalar optimization. When omitted,
// admission supplies the object. Its mode defaults to Optimize, its scalar metric defaults
// to ThroughputPerGPU, and its absent SLA defaults to TTFT 2000 ms and ITL 30 ms.
// +optional
Objective *ObjectiveSpec `json:"objective,omitempty"`

// AISimulate selects the execution artifact and supplies native AISimulate input.
// AISimulate supplies public recommendation input to the operator's AISimulate execution bundle.
// +optional
AISimulate *AISimulateSpec `json:"aisimulate,omitempty"`

Expand Down
Loading