diff --git a/deploy/operator/api/roundtrip_fuzz_test.go b/deploy/operator/api/roundtrip_fuzz_test.go index 1c2dd91cf6b2..9d313bf7cac5 100644 --- a/deploy/operator/api/roundtrip_fuzz_test.go +++ b/deploy/operator/api/roundtrip_fuzz_test.go @@ -179,6 +179,28 @@ func dynamoFuzzerFuncs(_ runtimeserializer.CodecFactory) []any { fuzzAlphaDGDRStatus, fuzzBetaDGDRSpec, fuzzBetaDGDRStatus, + // PlacementStatus (v1alpha1 + v1beta1): pick admissible values so the + // round-trip fuzzer exercises Placement without producing shapes the CRD + // schema rejects. State draws from the enum; Score draws either nil or a + // value inside the [0, 1] bounds enforced by the CRD. + func(p *v1beta1.PlacementStatus, c randfill.Continue) { + p.State = oneOf(c, + v1beta1.PlacementScoreStateReported, + v1beta1.PlacementScoreStatePartial, + v1beta1.PlacementScoreStateUnsupported, + v1beta1.PlacementScoreStateUnknown, + ) + p.Score = oneOfPtr(c, 0.0, 0.25, 0.5, 0.75, 1.0) + }, + func(p *v1alpha1.PlacementStatus, c randfill.Continue) { + p.State = oneOf(c, + v1alpha1.PlacementScoreStateReported, + v1alpha1.PlacementScoreStatePartial, + v1alpha1.PlacementScoreStateUnsupported, + v1alpha1.PlacementScoreStateUnknown, + ) + p.Score = oneOfPtr(c, 0.0, 0.25, 0.5, 0.75, 1.0) + }, // v1beta1 Components: the listMapKey marker requires name // to be non-empty and unique; MaxItems caps the length at 25. // Enforce both so the input is admissible. @@ -308,6 +330,17 @@ func oneOf[T any](c randfill.Continue, values ...T) T { return values[c.Intn(len(values))] } +// oneOfPtr returns nil roughly half the time; otherwise a pointer to one of +// values. Useful for optional API fields that must either be unset or draw +// from a constrained value set. +func oneOfPtr[T any](c randfill.Continue, values ...T) *T { + if c.Bool() { + return nil + } + v := oneOf(c, values...) + return &v +} + func fuzzJSONValue(c randfill.Continue, depth int) any { if depth >= 2 { switch c.Uint32() % 5 { diff --git a/deploy/operator/api/v1alpha1/conversion_field_coverage_test.go b/deploy/operator/api/v1alpha1/conversion_field_coverage_test.go index 4f338d8c5eba..90c1879770e6 100644 --- a/deploy/operator/api/v1alpha1/conversion_field_coverage_test.go +++ b/deploy/operator/api/v1alpha1/conversion_field_coverage_test.go @@ -156,6 +156,8 @@ DynamoGraphDeploymentStatus.components.scheduledReplicas DynamoGraphDeploymentStatus.components.updatedReplicas DynamoGraphDeploymentStatus.conditions DynamoGraphDeploymentStatus.observedGeneration +DynamoGraphDeploymentStatus.placement.score +DynamoGraphDeploymentStatus.placement.state DynamoGraphDeploymentStatus.restart.inProgress DynamoGraphDeploymentStatus.restart.observedID DynamoGraphDeploymentStatus.restart.phase diff --git a/deploy/operator/api/v1alpha1/dynamographdeployment_conversion.go b/deploy/operator/api/v1alpha1/dynamographdeployment_conversion.go index a0a5c1fbb7f7..3749c5a8e8f4 100644 --- a/deploy/operator/api/v1alpha1/dynamographdeployment_conversion.go +++ b/deploy/operator/api/v1alpha1/dynamographdeployment_conversion.go @@ -640,6 +640,16 @@ func ConvertToSpecTopologyConstraint(src *v1beta1.SpecTopologyConstraint, dst *S func ConvertFromDynamoGraphDeploymentStatus(src *DynamoGraphDeploymentStatus, dst *v1beta1.DynamoGraphDeploymentStatus) { dst.ObservedGeneration = src.ObservedGeneration dst.State = v1beta1.DGDState(src.State) + if src.Placement != nil { + dst.Placement = &v1beta1.PlacementStatus{ + State: v1beta1.PlacementScoreState(src.Placement.State), + } + if src.Placement.Score != nil { + dst.Placement.Score = ptr.To(*src.Placement.Score) + } + } else { + dst.Placement = nil + } if len(src.Conditions) > 0 { dst.Conditions = make([]metav1.Condition, 0, len(src.Conditions)) for _, c := range src.Conditions { @@ -677,6 +687,16 @@ func ConvertFromDynamoGraphDeploymentStatus(src *DynamoGraphDeploymentStatus, ds func ConvertToDynamoGraphDeploymentStatus(src *v1beta1.DynamoGraphDeploymentStatus, dst *DynamoGraphDeploymentStatus) { dst.ObservedGeneration = src.ObservedGeneration dst.State = DGDState(src.State) + if src.Placement != nil { + dst.Placement = &PlacementStatus{ + State: PlacementScoreState(src.Placement.State), + } + if src.Placement.Score != nil { + dst.Placement.Score = ptr.To(*src.Placement.Score) + } + } else { + dst.Placement = nil + } if len(src.Conditions) > 0 { dst.Conditions = make([]metav1.Condition, 0, len(src.Conditions)) for _, c := range src.Conditions { diff --git a/deploy/operator/api/v1alpha1/dynamographdeployment_conversion_test.go b/deploy/operator/api/v1alpha1/dynamographdeployment_conversion_test.go index 07d33293b322..6d1c1f684311 100644 --- a/deploy/operator/api/v1alpha1/dynamographdeployment_conversion_test.go +++ b/deploy/operator/api/v1alpha1/dynamographdeployment_conversion_test.go @@ -912,6 +912,10 @@ func TestDGD_RoundTrip_Status(t *testing.T) { Status: v1beta1.DynamoGraphDeploymentStatus{ ObservedGeneration: 7, State: v1beta1.DGDStateSuccessful, + Placement: &v1beta1.PlacementStatus{ + Score: ptr.To(0.87), + State: v1beta1.PlacementScoreStateReported, + }, Conditions: []metav1.Condition{ { Type: "Ready", diff --git a/deploy/operator/api/v1alpha1/dynamographdeployment_types.go b/deploy/operator/api/v1alpha1/dynamographdeployment_types.go index 300247a4d480..32c5e4e453ab 100644 --- a/deploy/operator/api/v1alpha1/dynamographdeployment_types.go +++ b/deploy/operator/api/v1alpha1/dynamographdeployment_types.go @@ -54,6 +54,34 @@ const ( DGDStateFailed DGDState = "failed" ) +// PlacementScoreState describes whether placement score is available and how +// complete the reported score is for a graph deployment. See the v1beta1 +// PlacementScoreState for the authoritative semantics of each value. +// +kubebuilder:validation:Enum=Reported;Partial;Unsupported;Unknown +type PlacementScoreState string + +const ( + PlacementScoreStateReported PlacementScoreState = "Reported" + PlacementScoreStatePartial PlacementScoreState = "Partial" + PlacementScoreStateUnsupported PlacementScoreState = "Unsupported" + PlacementScoreStateUnknown PlacementScoreState = "Unknown" +) + +// PlacementStatus groups DGD-level scheduler placement fields under a single +// status object so future placement signals can be added without a schema +// break. See the v1beta1 PlacementStatus for the authoritative field docs. +type PlacementStatus struct { + // Score is the DGD-level scheduler placement score. Normalized to [0.0, 1.0] + // where higher is better and 1.0 is the best possible placement. + // +optional + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1 + Score *float64 `json:"score,omitempty"` + // State indicates placement score reporting state. + // +optional + State PlacementScoreState `json:"state,omitempty"` +} + // DynamoGraphDeploymentSpec defines the desired state of DynamoGraphDeployment. // +kubebuilder:validation:XValidation:rule="oldSelf.hasValue() || !has(self.restart)",message="spec.restart must be unset on create; set spec.restart.id after creation to request a restart",optionalOldSelf=true type DynamoGraphDeploymentSpec struct { @@ -169,6 +197,10 @@ type DynamoGraphDeploymentStatus struct { // Currently only supported for singl-node, non-Grove deployments (DCD/Deployment). // +optional RollingUpdate *RollingUpdateStatus `json:"rollingUpdate,omitempty"` + // Placement groups DGD-level scheduler placement signals (score, reporting + // state, and any future placement fields). + // +optional + Placement *PlacementStatus `json:"placement,omitempty"` } // ServiceCheckpointStatus contains checkpoint information for a single service. diff --git a/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go b/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go index b0b21b20774f..ed2f2ab80449 100644 --- a/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go @@ -1052,6 +1052,11 @@ func (in *DynamoGraphDeploymentStatus) DeepCopyInto(out *DynamoGraphDeploymentSt *out = new(RollingUpdateStatus) (*in).DeepCopyInto(*out) } + if in.Placement != nil { + in, out := &in.Placement, &out.Placement + *out = new(PlacementStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamoGraphDeploymentStatus. @@ -1529,6 +1534,26 @@ func (in *PVC) DeepCopy() *PVC { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlacementStatus) DeepCopyInto(out *PlacementStatus) { + *out = *in + if in.Score != nil { + in, out := &in.Score, &out.Score + *out = new(float64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementStatus. +func (in *PlacementStatus) DeepCopy() *PlacementStatus { + if in == nil { + return nil + } + out := new(PlacementStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PodReference) DeepCopyInto(out *PodReference) { *out = *in diff --git a/deploy/operator/api/v1beta1/common.go b/deploy/operator/api/v1beta1/common.go index b3ba29bd53ac..756bf18a33c6 100644 --- a/deploy/operator/api/v1beta1/common.go +++ b/deploy/operator/api/v1beta1/common.go @@ -564,6 +564,53 @@ const ( DGDStateFailed DGDState = "failed" ) +// PlacementScoreState describes whether placement score is available and how +// complete the reported score is for a graph deployment. +// +// Every backend must set this field after the first reconciliation: +// - Reported: a score is available for every scored placement unit. +// - Partial: a score is available for some but not all placement units. +// - Unsupported: the backend does not surface a placement score at all. +// - Unknown: the backend supports scores but the current value is +// indeterminate (e.g. read failure, not yet populated by the +// scheduler). When set, PlacementStatus.Score must be cleared. +// +// +kubebuilder:validation:Enum=Reported;Partial;Unsupported;Unknown +type PlacementScoreState string + +const ( + PlacementScoreStateReported PlacementScoreState = "Reported" + PlacementScoreStatePartial PlacementScoreState = "Partial" + PlacementScoreStateUnsupported PlacementScoreState = "Unsupported" + PlacementScoreStateUnknown PlacementScoreState = "Unknown" +) + +// PlacementStatus groups DGD-level scheduler placement fields under a single +// status object so future placement signals (e.g. scheduler contract version, +// last-report timestamp, per-unit reports) can be added without a schema break. +// +// The score source is an open question in DEP #10064 (Grove mirror, typed Grove +// scheduler API, or unstructured provider). Until a source is selected and +// implemented, the DGD controller does not write this field; the schema and +// conversion are landed here so downstream consumers can rely on the shape. +type PlacementStatus struct { + // score is the DGD-level scheduler placement score aggregated from + // relevant scheduler placement units. Normalized to [0.0, 1.0] where higher + // is better and 1.0 represents the best possible placement. Aggregation + // uses the minimum across placement units so the value is a worst-placement + // signal for the graph. Scores are only comparable across DGDs that share + // the same scheduler scoring contract and version. + // +optional + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1 + Score *float64 `json:"score,omitempty"` + + // state indicates placement score reporting state. See PlacementScoreState + // for the semantics of each value. + // +optional + State PlacementScoreState `json:"state,omitempty"` +} + // RestartPhase enumerates phases of a graph-level restart. type RestartPhase string diff --git a/deploy/operator/api/v1beta1/dynamographdeployment_types.go b/deploy/operator/api/v1beta1/dynamographdeployment_types.go index 89cf9585fda0..78e532e9950e 100644 --- a/deploy/operator/api/v1beta1/dynamographdeployment_types.go +++ b/deploy/operator/api/v1beta1/dynamographdeployment_types.go @@ -126,6 +126,11 @@ type DynamoGraphDeploymentStatus struct { // Currently only supported for single-node, non-Grove deployments (DCD/Deployment). // +optional RollingUpdate *RollingUpdateStatus `json:"rollingUpdate,omitempty"` + + // placement groups DGD-level scheduler placement signals (score, reporting + // state, and any future placement fields). + // +optional + Placement *PlacementStatus `json:"placement,omitempty"` } // DGD Ready condition reasons used to classify Grove-backed not-ready diff --git a/deploy/operator/api/v1beta1/zz_generated.deepcopy.go b/deploy/operator/api/v1beta1/zz_generated.deepcopy.go index b9beb2e0c91c..dd8efdd35fac 100644 --- a/deploy/operator/api/v1beta1/zz_generated.deepcopy.go +++ b/deploy/operator/api/v1beta1/zz_generated.deepcopy.go @@ -810,6 +810,11 @@ func (in *DynamoGraphDeploymentStatus) DeepCopyInto(out *DynamoGraphDeploymentSt *out = new(RollingUpdateStatus) (*in).DeepCopyInto(*out) } + if in.Placement != nil { + in, out := &in.Placement, &out.Placement + *out = new(PlacementStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamoGraphDeploymentStatus. @@ -1131,6 +1136,26 @@ func (in *ParetoConfig) DeepCopy() *ParetoConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlacementStatus) DeepCopyInto(out *PlacementStatus) { + *out = *in + if in.Score != nil { + in, out := &in.Score, &out.Score + *out = new(float64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementStatus. +func (in *PlacementStatus) DeepCopy() *PlacementStatus { + if in == nil { + return nil + } + out := new(PlacementStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProfilingResultsStatus) DeepCopyInto(out *ProfilingResultsStatus) { *out = *in diff --git a/deploy/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml b/deploy/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml index fd90a86359c0..f39545afcf05 100644 --- a/deploy/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml +++ b/deploy/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml @@ -11951,6 +11951,27 @@ spec: description: ObservedGeneration is the most recent generation observed by the controller. format: int64 type: integer + placement: + description: |- + Placement groups DGD-level scheduler placement signals (score, reporting + state, and any future placement fields). + properties: + score: + description: |- + Score is the DGD-level scheduler placement score. Normalized to [0.0, 1.0] + where higher is better and 1.0 is the best possible placement. + maximum: 1 + minimum: 0 + type: number + state: + description: State indicates placement score reporting state. + enum: + - Reported + - Partial + - Unsupported + - Unknown + type: string + type: object restart: description: Restart contains the status of the restart of the graph deployment. properties: @@ -21370,6 +21391,33 @@ spec: description: observedGeneration is the most recent generation observed by the controller. format: int64 type: integer + placement: + description: |- + placement groups DGD-level scheduler placement signals (score, reporting + state, and any future placement fields). + properties: + score: + description: |- + score is the DGD-level scheduler placement score aggregated from + relevant scheduler placement units. Normalized to [0.0, 1.0] where higher + is better and 1.0 represents the best possible placement. Aggregation + uses the minimum across placement units so the value is a worst-placement + signal for the graph. Scores are only comparable across DGDs that share + the same scheduler scoring contract and version. + maximum: 1 + minimum: 0 + type: number + state: + description: |- + state indicates placement score reporting state. See PlacementScoreState + for the semantics of each value. + enum: + - Reported + - Partial + - Unsupported + - Unknown + type: string + type: object restart: description: restart contains the status of a graph-level restart. properties: diff --git a/docs/kubernetes/api-reference.md b/docs/kubernetes/api-reference.md index c5fd2036e848..9bad1f3ec8fe 100644 --- a/docs/kubernetes/api-reference.md +++ b/docs/kubernetes/api-reference.md @@ -738,6 +738,7 @@ _Appears in:_ | `restart` _[RestartStatus](#restartstatus)_ | Restart contains the status of the restart of the graph deployment. | | Optional: \{\}
| | `checkpoints` _object (keys:string, values:[ServiceCheckpointStatus](#servicecheckpointstatus))_ | Checkpoints contains per-service checkpoint status information.
The map key is the service name from spec.services. | | Optional: \{\}
| | `rollingUpdate` _[RollingUpdateStatus](#rollingupdatestatus)_ | RollingUpdate tracks the progress of operator manged rolling updates.
Currently only supported for singl-node, non-Grove deployments (DCD/Deployment). | | Optional: \{\}
| +| `placement` _[PlacementStatus](#placementstatus)_ | Placement groups DGD-level scheduler placement signals (score, reporting
state, and any future placement fields). | | Optional: \{\}
| #### DynamoModel @@ -1127,6 +1128,47 @@ _Appears in:_ | `volumeAccessMode` _[PersistentVolumeAccessMode](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#persistentvolumeaccessmode-v1-core)_ | VolumeAccessMode is the volume access mode of the PVC. Required when create is true. | | | +#### PlacementScoreState + +_Underlying type:_ _string_ + +PlacementScoreState describes whether placement score is available and how +complete the reported score is for a graph deployment. See the v1beta1 +PlacementScoreState for the authoritative semantics of each value. + +_Validation:_ +- Enum: [Reported Partial Unsupported Unknown] + +_Appears in:_ +- [PlacementStatus](#placementstatus) + +| Field | Description | +| --- | --- | +| `Reported` | | +| `Partial` | | +| `Unsupported` | | +| `Unknown` | | + + +#### PlacementStatus + + + +PlacementStatus groups DGD-level scheduler placement fields under a single +status object so future placement signals can be added without a schema +break. See the v1beta1 PlacementStatus for the authoritative field docs. + + + +_Appears in:_ +- [DynamoGraphDeploymentStatus](#dynamographdeploymentstatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `score` _float_ | Score is the DGD-level scheduler placement score. Normalized to [0.0, 1.0]
where higher is better and 1.0 is the best possible placement. | | Maximum: 1
Minimum: 0
Optional: \{\}
| +| `state` _[PlacementScoreState](#placementscorestate)_ | State indicates placement score reporting state. | | Enum: [Reported Partial Unsupported Unknown]
Optional: \{\}
| + + #### PodReference @@ -2376,6 +2418,7 @@ _Appears in:_ | `restart` _[RestartStatus](#restartstatus)_ | restart contains the status of a graph-level restart. | | Optional: \{\}
| | `checkpoints` _object (keys:string, values:[ComponentCheckpointStatus](#componentcheckpointstatus))_ | checkpoints contains per-component checkpoint status, keyed by component name. | | Optional: \{\}
| | `rollingUpdate` _[RollingUpdateStatus](#rollingupdatestatus)_ | rollingUpdate tracks the progress of operator-managed rolling updates.
Currently only supported for single-node, non-Grove deployments (DCD/Deployment). | | Optional: \{\}
| +| `placement` _[PlacementStatus](#placementstatus)_ | placement groups DGD-level scheduler placement signals (score, reporting
state, and any future placement fields). | | Optional: \{\}
| #### EPPConfig @@ -2743,6 +2786,59 @@ _Appears in:_ | `config` _[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#rawextension-runtime-pkg)_ | Config is the full deployment configuration for this Pareto point. | | Type: object
| +#### PlacementScoreState + +_Underlying type:_ _string_ + +PlacementScoreState describes whether placement score is available and how +complete the reported score is for a graph deployment. + +Every backend must set this field after the first reconciliation: + - Reported: a score is available for every scored placement unit. + - Partial: a score is available for some but not all placement units. + - Unsupported: the backend does not surface a placement score at all. + - Unknown: the backend supports scores but the current value is + indeterminate (e.g. read failure, not yet populated by the + scheduler). When set, PlacementStatus.Score must be cleared. + +_Validation:_ +- Enum: [Reported Partial Unsupported Unknown] + +_Appears in:_ +- [PlacementStatus](#placementstatus) + +| Field | Description | +| --- | --- | +| `Reported` | | +| `Partial` | | +| `Unsupported` | | +| `Unknown` | | + + +#### PlacementStatus + + + +PlacementStatus groups DGD-level scheduler placement fields under a single +status object so future placement signals (e.g. scheduler contract version, +last-report timestamp, per-unit reports) can be added without a schema break. + +The score source is an open question in DEP #10064 (Grove mirror, typed Grove +scheduler API, or unstructured provider). Until a source is selected and +implemented, the DGD controller does not write this field; the schema and +conversion are landed here so downstream consumers can rely on the shape. + + + +_Appears in:_ +- [DynamoGraphDeploymentStatus](#dynamographdeploymentstatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `score` _float_ | score is the DGD-level scheduler placement score aggregated from
relevant scheduler placement units. Normalized to [0.0, 1.0] where higher
is better and 1.0 represents the best possible placement. Aggregation
uses the minimum across placement units so the value is a worst-placement
signal for the graph. Scores are only comparable across DGDs that share
the same scheduler scoring contract and version. | | Maximum: 1
Minimum: 0
Optional: \{\}
| +| `state` _[PlacementScoreState](#placementscorestate)_ | state indicates placement score reporting state. See PlacementScoreState
for the semantics of each value. | | Enum: [Reported Partial Unsupported Unknown]
Optional: \{\}
| + + #### ProfilingPhase _Underlying type:_ _string_