diff --git a/deploy/operator/api/v1alpha1/conversion_field_coverage_test.go b/deploy/operator/api/v1alpha1/conversion_field_coverage_test.go
index 3a15a1367e2b..c10c84f302c8 100644
--- a/deploy/operator/api/v1alpha1/conversion_field_coverage_test.go
+++ b/deploy/operator/api/v1alpha1/conversion_field_coverage_test.go
@@ -58,6 +58,7 @@ DynamoComponentDeploymentSpec.experimental.gpuMemoryService.extraClientContainer
DynamoComponentDeploymentSpec.experimental.gpuMemoryService.extraClientPods.name
DynamoComponentDeploymentSpec.experimental.gpuMemoryService.extraClientPods.podTemplate
DynamoComponentDeploymentSpec.experimental.gpuMemoryService.mode
+DynamoComponentDeploymentSpec.experimental.grove.forceScalingGroup
DynamoComponentDeploymentSpec.frontendSidecar
DynamoComponentDeploymentSpec.globalDynamoNamespace
DynamoComponentDeploymentSpec.minAvailable
@@ -117,6 +118,7 @@ DynamoGraphDeploymentSpec.components.experimental.gpuMemoryService.extraClientCo
DynamoGraphDeploymentSpec.components.experimental.gpuMemoryService.extraClientPods.name
DynamoGraphDeploymentSpec.components.experimental.gpuMemoryService.extraClientPods.podTemplate
DynamoGraphDeploymentSpec.components.experimental.gpuMemoryService.mode
+DynamoGraphDeploymentSpec.components.experimental.grove.forceScalingGroup
DynamoGraphDeploymentSpec.components.frontendSidecar
DynamoGraphDeploymentSpec.components.globalDynamoNamespace
DynamoGraphDeploymentSpec.components.minAvailable
diff --git a/deploy/operator/api/v1alpha1/dynamocomponentdeployment_conversion_test.go b/deploy/operator/api/v1alpha1/dynamocomponentdeployment_conversion_test.go
index cb2424be08a3..cde9a0f05e00 100644
--- a/deploy/operator/api/v1alpha1/dynamocomponentdeployment_conversion_test.go
+++ b/deploy/operator/api/v1alpha1/dynamocomponentdeployment_conversion_test.go
@@ -623,6 +623,48 @@ func TestDCD_RoundTrip_Experimental(t *testing.T) {
}
}
+// The grove block has no v1alpha1 representation and must survive the spoke
+// round-trip both alone (whole hub-only block preserved) and alongside
+// alpha-representable fields (sparse preservation merged back).
+func TestDCD_RoundTrip_ExperimentalGrove(t *testing.T) {
+ tests := []struct {
+ name string
+ experimental *v1beta1.ExperimentalSpec
+ }{
+ {
+ name: "grove.forceScalingGroup only",
+ experimental: &v1beta1.ExperimentalSpec{
+ Grove: &v1beta1.GroveSpec{ForceScalingGroup: true},
+ },
+ },
+ {
+ name: "grove.forceScalingGroup alongside alpha-representable GMS",
+ experimental: &v1beta1.ExperimentalSpec{
+ GPUMemoryService: &v1beta1.GPUMemoryServiceSpec{Mode: v1beta1.GMSModeIntraPod},
+ Grove: &v1beta1.GroveSpec{ForceScalingGroup: true},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ src := &v1beta1.DynamoComponentDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "exp-grouping", Namespace: "ns"},
+ Spec: v1beta1.DynamoComponentDeploymentSpec{
+ DynamoComponentDeploymentSharedSpec: v1beta1.DynamoComponentDeploymentSharedSpec{
+ ComponentName: "exp-grouping",
+ ComponentType: v1beta1.ComponentTypeWorker,
+ Experimental: tt.experimental.DeepCopy(),
+ },
+ },
+ }
+ got := dcdRoundTripFromV1beta1(t, src)
+ if diff := cmp.Diff(src, got, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("round-trip mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
+
func TestDCD_ExperimentalModeValuesAreValidForIntermediateVersion(t *testing.T) {
alpha := &DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{Name: "alpha-enums", Namespace: "ns"},
diff --git a/deploy/operator/api/v1alpha1/shared_spec_conversion.go b/deploy/operator/api/v1alpha1/shared_spec_conversion.go
index 296548c71d75..8d2af9971d64 100644
--- a/deploy/operator/api/v1alpha1/shared_spec_conversion.go
+++ b/deploy/operator/api/v1alpha1/shared_spec_conversion.go
@@ -596,6 +596,10 @@ func saveSharedHubOnlySpec(src *v1beta1.DynamoComponentDeploymentSharedSpec, con
}
if experimentalIsHubOnlyShape(src.Experimental) {
save.Experimental = src.Experimental.DeepCopy()
+ } else if src.Experimental != nil && src.Experimental.Grove != nil {
+ // The grove block has no v1alpha1 representation; preserve it sparsely
+ // when the rest of the experimental block converts to alpha fields.
+ save.Experimental = &v1beta1.ExperimentalSpec{Grove: src.Experimental.Grove.DeepCopy()}
}
return nil
}
@@ -1618,6 +1622,11 @@ func restoreSharedHubOnlyFields(dst, preserved *v1beta1.DynamoComponentDeploymen
restoreSharedHubOnlyFrontendSidecar(dst, preserved)
if dst.Experimental == nil && experimentalIsHubOnlyShape(preserved.Experimental) {
dst.Experimental = preserved.Experimental.DeepCopy()
+ } else if dst.Experimental != nil && preserved.Experimental != nil &&
+ dst.Experimental.Grove == nil && preserved.Experimental.Grove != nil {
+ // The experimental block was rebuilt from alpha fields (GMS, failover,
+ // checkpoint); merge back the sparsely preserved hub-only grove block.
+ dst.Experimental.Grove = preserved.Experimental.Grove.DeepCopy()
}
return nil
}
diff --git a/deploy/operator/api/v1beta1/common.go b/deploy/operator/api/v1beta1/common.go
index a329f2a76f7e..f1fa4a834871 100644
--- a/deploy/operator/api/v1beta1/common.go
+++ b/deploy/operator/api/v1beta1/common.go
@@ -164,6 +164,22 @@ const (
GMSModeInterPod GPUMemoryServiceMode = "InterPod"
)
+// GroveSpec groups experimental Grove-specific rendering options.
+type GroveSpec struct {
+ // forceScalingGroup opts a single-node component into rendering as a
+ // PodCliqueScalingGroup with one single-pod PodClique per replica.
+ // Scaling changes the scaling-group replica count. The first
+ // `minAvailable` replicas join the deployment's base PodGang together
+ // with its other base workloads; each replica beyond `minAvailable`
+ // gets its own PodGang, gang-scheduled separately from the rest of the
+ // deployment. `false` or omitted means automatic selection (multi-node
+ // and inter-pod GMS components use a scaling group, other single-node
+ // components a standalone PodClique), not "force PodClique".
+ // Immutable after creation.
+ // +optional
+ ForceScalingGroup bool `json:"forceScalingGroup,omitempty"`
+}
+
// ExperimentalSpec groups opt-in preview features whose API shape and behavior
// may change in breaking ways between v1beta1 releases (including disappearing
// without a name-preserving graduation path). Fields placed under
@@ -183,6 +199,10 @@ type ExperimentalSpec struct {
// +optional
Failover *FailoverSpec `json:"failover,omitempty"`
+ // grove groups Grove-specific rendering options.
+ // +optional
+ Grove *GroveSpec `json:"grove,omitempty"`
+
// checkpoint configures container-image snapshotting and restore for
// this component. Set `checkpoint.enabled: true` to opt in. Without
// checkpointRef, the DGD controller creates a DGD-scoped DynamoCheckpoint
diff --git a/deploy/operator/api/v1beta1/dynamocomponentdeployment_types.go b/deploy/operator/api/v1beta1/dynamocomponentdeployment_types.go
index 84afeedf3294..1797b0151b92 100644
--- a/deploy/operator/api/v1beta1/dynamocomponentdeployment_types.go
+++ b/deploy/operator/api/v1beta1/dynamocomponentdeployment_types.go
@@ -133,8 +133,10 @@ type DynamoComponentDeploymentSharedSpec struct {
// +optional
Replicas *int32 `json:"replicas,omitempty"`
- // minAvailable maps to Grove PodClique minAvailable for single-node and
- // Grove PodCliqueScalingGroup minAvailable for multi-node components.
+ // minAvailable maps to Grove PodCliqueScalingGroup minAvailable for
+ // components rendered as a scaling group (multi-node, inter-pod GMS, or
+ // `experimental.grove.forceScalingGroup`; see `UsesPCSG`) and to Grove
+ // PodClique minAvailable for all other single-node components.
// This field determines 1) the minimum number of replicas guaranteed to be
// gang-scheduled, and 2) when violating minAvailable replicas triggers gang
// termination.
@@ -327,6 +329,19 @@ func (s *DynamoComponentDeploymentSharedSpec) IsInterPodGMSEnabled() bool {
s.Experimental.GPUMemoryService.Mode == GMSModeInterPod
}
+// IsGroveScalingGroupForced reports whether the ScalingGroup layout is explicitly requested.
+func (s *DynamoComponentDeploymentSharedSpec) IsGroveScalingGroupForced() bool {
+ return s.Experimental != nil &&
+ s.Experimental.Grove != nil &&
+ s.Experimental.Grove.ForceScalingGroup
+}
+
+// UsesPCSG reports whether Grove renders this component as a
+// PodCliqueScalingGroup rather than a standalone PodClique.
+func (s *DynamoComponentDeploymentSharedSpec) UsesPCSG() bool {
+ return s.GetNumberOfNodes() > 1 || s.IsInterPodGMSEnabled() || s.IsGroveScalingGroupForced()
+}
+
// IsInterPodFailoverEnabled reports whether inter-pod GMS failover is configured.
func (s *DynamoComponentDeploymentSharedSpec) IsInterPodFailoverEnabled() bool {
return s.Experimental != nil &&
diff --git a/deploy/operator/api/v1beta1/zz_generated.deepcopy.go b/deploy/operator/api/v1beta1/zz_generated.deepcopy.go
index dd8efdd35fac..11020af56964 100644
--- a/deploy/operator/api/v1beta1/zz_generated.deepcopy.go
+++ b/deploy/operator/api/v1beta1/zz_generated.deepcopy.go
@@ -865,6 +865,11 @@ func (in *ExperimentalSpec) DeepCopyInto(out *ExperimentalSpec) {
*out = new(FailoverSpec)
**out = **in
}
+ if in.Grove != nil {
+ in, out := &in.Grove, &out.Grove
+ *out = new(GroveSpec)
+ **out = **in
+ }
if in.Checkpoint != nil {
in, out := &in.Checkpoint, &out.Checkpoint
*out = new(ComponentCheckpointConfig)
@@ -965,6 +970,21 @@ func (in *GPUMemoryServiceSpec) DeepCopy() *GPUMemoryServiceSpec {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GroveSpec) DeepCopyInto(out *GroveSpec) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroveSpec.
+func (in *GroveSpec) DeepCopy() *GroveSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(GroveSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HardwareSpec) DeepCopyInto(out *HardwareSpec) {
*out = *in
diff --git a/deploy/operator/config/crd/bases/nvidia.com_dynamocomponentdeployments.yaml b/deploy/operator/config/crd/bases/nvidia.com_dynamocomponentdeployments.yaml
index f71d6d84ab1e..947a2def3788 100644
--- a/deploy/operator/config/crd/bases/nvidia.com_dynamocomponentdeployments.yaml
+++ b/deploy/operator/config/crd/bases/nvidia.com_dynamocomponentdeployments.yaml
@@ -12186,6 +12186,23 @@ spec:
rule: '!has(self.extraClientPods) || size(self.extraClientPods) == 0 || self.mode == ''InterPod'''
- message: extraClientPods is reserved for inter-pod GMS and is not implemented yet
rule: '!has(self.extraClientPods) || size(self.extraClientPods) == 0'
+ grove:
+ description: grove groups Grove-specific rendering options.
+ properties:
+ forceScalingGroup:
+ description: |-
+ forceScalingGroup opts a single-node component into rendering as a
+ PodCliqueScalingGroup with one single-pod PodClique per replica.
+ Scaling changes the scaling-group replica count. The first
+ `minAvailable` replicas join the deployment's base PodGang together
+ with its other base workloads; each replica beyond `minAvailable`
+ gets its own PodGang, gang-scheduled separately from the rest of the
+ deployment. `false` or omitted means automatic selection (multi-node
+ and inter-pod GMS components use a scaling group, other single-node
+ components a standalone PodClique), not "force PodClique".
+ Immutable after creation.
+ type: boolean
+ type: object
type: object
frontendSidecar:
description: |-
@@ -12208,8 +12225,10 @@ spec:
type: boolean
minAvailable:
description: |-
- minAvailable maps to Grove PodClique minAvailable for single-node and
- Grove PodCliqueScalingGroup minAvailable for multi-node components.
+ minAvailable maps to Grove PodCliqueScalingGroup minAvailable for
+ components rendered as a scaling group (multi-node, inter-pod GMS, or
+ `experimental.grove.forceScalingGroup`; see `UsesPCSG`) and to Grove
+ PodClique minAvailable for all other single-node components.
This field determines 1) the minimum number of replicas guaranteed to be
gang-scheduled, and 2) when violating minAvailable replicas triggers gang
termination.
diff --git a/deploy/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml b/deploy/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml
index f9465892fd73..13c480c90e99 100644
--- a/deploy/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml
+++ b/deploy/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml
@@ -12644,6 +12644,23 @@ spec:
rule: '!has(self.extraClientPods) || size(self.extraClientPods) == 0 || self.mode == ''InterPod'''
- message: extraClientPods is reserved for inter-pod GMS and is not implemented yet
rule: '!has(self.extraClientPods) || size(self.extraClientPods) == 0'
+ grove:
+ description: grove groups Grove-specific rendering options.
+ properties:
+ forceScalingGroup:
+ description: |-
+ forceScalingGroup opts a single-node component into rendering as a
+ PodCliqueScalingGroup with one single-pod PodClique per replica.
+ Scaling changes the scaling-group replica count. The first
+ `minAvailable` replicas join the deployment's base PodGang together
+ with its other base workloads; each replica beyond `minAvailable`
+ gets its own PodGang, gang-scheduled separately from the rest of the
+ deployment. `false` or omitted means automatic selection (multi-node
+ and inter-pod GMS components use a scaling group, other single-node
+ components a standalone PodClique), not "force PodClique".
+ Immutable after creation.
+ type: boolean
+ type: object
type: object
frontendSidecar:
description: |-
@@ -12666,8 +12683,10 @@ spec:
type: boolean
minAvailable:
description: |-
- minAvailable maps to Grove PodClique minAvailable for single-node and
- Grove PodCliqueScalingGroup minAvailable for multi-node components.
+ minAvailable maps to Grove PodCliqueScalingGroup minAvailable for
+ components rendered as a scaling group (multi-node, inter-pod GMS, or
+ `experimental.grove.forceScalingGroup`; see `UsesPCSG`) and to Grove
+ PodClique minAvailable for all other single-node components.
This field determines 1) the minimum number of replicas guaranteed to be
gang-scheduled, and 2) when violating minAvailable replicas triggers gang
termination.
diff --git a/deploy/operator/internal/controller/dgd_grove_restart_progress_resolver.go b/deploy/operator/internal/controller/dgd_grove_restart_progress_resolver.go
index 8fe171200ef0..367f3e0c494a 100644
--- a/deploy/operator/internal/controller/dgd_grove_restart_progress_resolver.go
+++ b/deploy/operator/internal/controller/dgd_grove_restart_progress_resolver.go
@@ -82,7 +82,7 @@ func (r *groveRestartProgressResolver) Resolve(
// Any component represented by a PodCliqueScalingGroup must use the
// PCSG readiness path. Read failures conservatively keep the component
// in progress; authoritative readiness returns the error separately.
- if component.GetNumberOfNodes() > 1 || component.IsInterPodGMSEnabled() {
+ if component.UsesPCSG() {
isReady, reason, _, _, _ = dynamo.CheckPCSGReady(
ctx,
r.reader,
diff --git a/deploy/operator/internal/controller/dgd_grove_scaler.go b/deploy/operator/internal/controller/dgd_grove_scaler.go
index 1f26724a397d..8ca110f79389 100644
--- a/deploy/operator/internal/controller/dgd_grove_scaler.go
+++ b/deploy/operator/internal/controller/dgd_grove_scaler.go
@@ -70,7 +70,7 @@ func (s *groveScaler) Reconcile(
replicas = 0
}
- usesPCSG := component.GetNumberOfNodes() > 1 || component.IsInterPodGMSEnabled()
+ usesPCSG := component.UsesPCSG()
resourceName := dynamo.GroveComponentResourceName(dgd, componentName)
resourceKind := "PodClique"
gvr := consts.PodCliqueGVR
diff --git a/deploy/operator/internal/dynamo/graph.go b/deploy/operator/internal/dynamo/graph.go
index 669f20678a56..d98359a5c522 100644
--- a/deploy/operator/internal/dynamo/graph.go
+++ b/deploy/operator/internal/dynamo/graph.go
@@ -1161,6 +1161,8 @@ const (
// multiple ServiceRoles depending on the deployment topology:
//
// - single-node, no GMS: 1 role (RoleMain)
+// - single-node, forceScalingGroup: 1 role (RoleMain with a single pod;
+// the PCSG replica count carries the horizontal scale)
// - multinode, no GMS: 2 roles (RoleLeader + RoleWorker)
// - single-node, inter-pod GMS: 1 engine PCLQ (replicated) + 1 RoleGMS
// weight-server PCLQ
@@ -1218,6 +1220,8 @@ func expandRolesForComponent(componentName string, componentReplicas *int32, num
return expandMultinodeRoles(componentName, numberOfNodes)
case isInterPodGMS:
return expandSingleNodeGMSRoles(componentName, component.GetTotalEnginePods())
+ case component.IsGroveScalingGroupForced():
+ return expandSingleNodeScalingGroupRoles(componentName)
default:
return expandSingleNodeRoles(componentName, componentReplicas)
}
@@ -1233,6 +1237,12 @@ func expandSingleNodeRoles(componentName string, componentReplicas *int32) []Ser
}
}
+func expandSingleNodeScalingGroupRoles(componentName string) []ServiceRole {
+ return []ServiceRole{
+ {Name: componentName, Role: RoleMain, Replicas: 1},
+ }
+}
+
func expandMultinodeRoles(componentName string, numberOfNodes int32) []ServiceRole {
return []ServiceRole{
{Name: componentName + "-" + commonconsts.GroveRoleSuffixLeader, Role: RoleLeader, Replicas: 1},
@@ -1274,7 +1284,7 @@ func LongestPodCliqueNameForDGDComponent(
component *v1beta1.DynamoComponentDeploymentSharedSpec,
) string {
lowerComponentName := strings.ToLower(componentName)
- if component == nil || (component.GetNumberOfNodes() <= 1 && !component.IsInterPodGMSEnabled()) {
+ if component == nil || !component.UsesPCSG() {
return lowerComponentName
}
@@ -1301,7 +1311,7 @@ func PCSNameForDGD(dgdName string, components []v1beta1.DynamoComponentDeploymen
componentName := component.ComponentName
lowerName := strings.ToLower(componentName)
var budget int
- if component.GetNumberOfNodes() > 1 || component.IsInterPodGMSEnabled() {
+ if component.UsesPCSG() {
// PCSG = lowerName, PCLQ = longest rendered role name.
budget = len(lowerName) + len(LongestPodCliqueNameForDGDComponent(componentName, component))
} else {
@@ -2537,7 +2547,7 @@ func GenerateGrovePodCliqueSet(
isMultinode := numberOfNodes > 1
isInterPodGMS := component.IsInterPodGMSEnabled()
isInterPodFailover := component.IsInterPodFailoverEnabled()
- usesPCSG := isMultinode || isInterPodGMS
+ usesPCSG := component.UsesPCSG()
roles := expandRolesForComponent(componentName, component.Replicas, numberOfNodes, component)
var cliqueNames []string
diff --git a/deploy/operator/internal/dynamo/graph_test.go b/deploy/operator/internal/dynamo/graph_test.go
index b3d6d2030d65..a2036f508f81 100644
--- a/deploy/operator/internal/dynamo/graph_test.go
+++ b/deploy/operator/internal/dynamo/graph_test.go
@@ -5082,6 +5082,23 @@ func TestExpandRolesForService(t *testing.T) {
}
}
+// forceScalingGroup is v1beta1-only, so it gets its own case instead of a
+// row in the alpha-shaped table above: the engine PCLQ holds one pod
+// regardless of the component replica count (the PCSG carries the scale).
+func TestExpandRolesForComponent_SingleNodeForceScalingGroup(t *testing.T) {
+ component := &v1beta1.DynamoComponentDeploymentSharedSpec{
+ Replicas: ptr.To(int32(4)),
+ Experimental: &v1beta1.ExperimentalSpec{
+ Grove: &v1beta1.GroveSpec{ForceScalingGroup: true},
+ },
+ }
+ got := expandRolesForComponent("svc", component.Replicas, 1, component)
+ want := []ServiceRole{{Name: "svc", Role: RoleMain, Replicas: 1}}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("expandRolesForComponent() = %v, want %v", got, want)
+ }
+}
+
func TestRoleEnum(t *testing.T) {
// Test that role constants are defined correctly
if RoleLeader != "leader" {
@@ -8780,6 +8797,62 @@ func TestGenerateGrovePodCliqueSet_ComponentMinAvailable(t *testing.T) {
}
}
+// TestGenerateGrovePodCliqueSet_SingleNodeForceScalingGroup pins the
+// experimental grove.forceScalingGroup opt-in: a single-node component
+// renders as a PCSG whose replica count carries the horizontal scale, with a
+// single one-pod engine PCLQ per PCSG replica.
+func TestGenerateGrovePodCliqueSet_SingleNodeForceScalingGroup(t *testing.T) {
+ dgd := &v1alpha1.DynamoGraphDeployment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-dgd", Namespace: "test-ns"},
+ Spec: v1alpha1.DynamoGraphDeploymentSpec{
+ BackendFramework: "vllm",
+ Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
+ "worker": {
+ ComponentType: commonconsts.ComponentTypeWorker,
+ Replicas: ptr.To(int32(4)),
+ MinAvailable: ptr.To(int32(2)),
+ Resources: &v1alpha1.Resources{
+ Limits: &v1alpha1.ResourceItem{GPU: "1"},
+ },
+ },
+ },
+ },
+ }
+
+ // grove.forceScalingGroup is v1beta1-only, so set it after conversion.
+ beta := betaDGD(t, dgd)
+ require.Len(t, beta.Spec.Components, 1)
+ beta.Spec.Components[0].Experimental = &v1beta1.ExperimentalSpec{
+ Grove: &v1beta1.GroveSpec{ForceScalingGroup: true},
+ }
+
+ got, err := GenerateGrovePodCliqueSet(
+ context.Background(),
+ beta,
+ &configv1alpha1.OperatorConfiguration{},
+ &controller_common.RuntimeConfig{},
+ nil, nil, nil, nil, nil,
+ )
+ require.NoError(t, err)
+ require.NotNil(t, got)
+
+ require.Len(t, got.Spec.Template.Cliques, 1)
+ clique := got.Spec.Template.Cliques[0]
+ assert.Equal(t, "worker", clique.Name)
+ assert.EqualValues(t, 1, clique.Spec.Replicas, "engine PCLQ must hold exactly one pod per PCSG replica")
+ require.NotNil(t, clique.Spec.MinAvailable)
+ assert.EqualValues(t, 1, *clique.Spec.MinAvailable)
+
+ require.Len(t, got.Spec.Template.PodCliqueScalingGroupConfigs, 1)
+ pcsg := got.Spec.Template.PodCliqueScalingGroupConfigs[0]
+ assert.Equal(t, "worker", pcsg.Name)
+ assert.Equal(t, []string{"worker"}, pcsg.CliqueNames)
+ require.NotNil(t, pcsg.Replicas)
+ assert.EqualValues(t, 4, *pcsg.Replicas, "PCSG replicas must carry the component replica count")
+ require.NotNil(t, pcsg.MinAvailable)
+ assert.EqualValues(t, 2, *pcsg.MinAvailable)
+}
+
// TestGenerateGrovePodCliqueSet_MinAvailable_FailoverShadowsAreRedundant pins
// the contract that per-rank engine cliques in an inter-pod failover cohort
// use MinAvailable=1 even when multinode (numberOfNodes > 1). Replicas here
diff --git a/deploy/operator/internal/dynamo/grove.go b/deploy/operator/internal/dynamo/grove.go
index 9eaf85ef16b5..e92a650f589c 100644
--- a/deploy/operator/internal/dynamo/grove.go
+++ b/deploy/operator/internal/dynamo/grove.go
@@ -161,7 +161,7 @@ func evaluateGroveComponents(ctx context.Context, reader client.Reader, dgd *v1b
for i := range dgd.Spec.Components {
component := &dgd.Spec.Components[i]
componentName := component.ComponentName
- usesPCSG := component.GetNumberOfNodes() > 1 || component.IsInterPodGMSEnabled()
+ usesPCSG := component.UsesPCSG()
resourceName := GroveComponentResourceName(dgd, componentName)
var ok bool
diff --git a/deploy/operator/internal/webhook/validation/dynamographdeployment_helpers.go b/deploy/operator/internal/webhook/validation/dynamographdeployment_helpers.go
index eed5c2e5b122..3d6e3af3288a 100644
--- a/deploy/operator/internal/webhook/validation/dynamographdeployment_helpers.go
+++ b/deploy/operator/internal/webhook/validation/dynamographdeployment_helpers.go
@@ -161,7 +161,7 @@ func dgdComponentResourceNameLength(
combinedLength := len(pcsName) + len(strings.ToLower(componentName))
detail := "PCS name + component name"
- if component.GetNumberOfNodes() > 1 || component.IsInterPodGMSEnabled() {
+ if component.UsesPCSG() {
longestPodCliqueName := dynamo.LongestPodCliqueNameForDGDComponent(componentName, component)
combinedLength += len(longestPodCliqueName)
detail = fmt.Sprintf("PCS name + PCSG name + longest PodClique name %q", longestPodCliqueName)
diff --git a/deploy/operator/internal/webhook/validation/dynamographdeployment_validation_envtest_test.go b/deploy/operator/internal/webhook/validation/dynamographdeployment_validation_envtest_test.go
index f4a554689363..ec3f23ae1378 100644
--- a/deploy/operator/internal/webhook/validation/dynamographdeployment_validation_envtest_test.go
+++ b/deploy/operator/internal/webhook/validation/dynamographdeployment_validation_envtest_test.go
@@ -794,6 +794,36 @@ func TestDynamoGraphDeploymentValidator_Validate(t *testing.T) {
}),
},
+ // Grove forceScalingGroup rules.
+ {
+ name: "component grove.forceScalingGroup requires Grove",
+ groveDisabled: true,
+ deployment: betaDGDForAdmission(func(dgd *nvidiacomv1beta1.DynamoGraphDeployment) {
+ betaWorkerComponent(dgd).Experimental = &nvidiacomv1beta1.ExperimentalSpec{
+ Grove: &nvidiacomv1beta1.GroveSpec{ForceScalingGroup: true},
+ }
+ }),
+ wantWebhookErrs: []string{"spec.components[1].experimental.grove.forceScalingGroup: Forbidden: is currently supported only for Grove-backed DynamoGraphDeployment components"},
+ },
+ {
+ name: "v1beta1 grove.forceScalingGroup on a single-node component reaches the webhook",
+ deployment: betaDGDForAdmission(func(dgd *nvidiacomv1beta1.DynamoGraphDeployment) {
+ betaWorkerComponent(dgd).Experimental = &nvidiacomv1beta1.ExperimentalSpec{
+ Grove: &nvidiacomv1beta1.GroveSpec{ForceScalingGroup: true},
+ }
+ }),
+ },
+ {
+ name: "v1beta1 redundant grove.forceScalingGroup on a multinode component reaches the webhook",
+ deployment: betaDGDForAdmission(func(dgd *nvidiacomv1beta1.DynamoGraphDeployment) {
+ worker := betaWorkerComponent(dgd)
+ worker.Multinode = &nvidiacomv1beta1.MultinodeSpec{NodeCount: 2}
+ worker.Experimental = &nvidiacomv1beta1.ExperimentalSpec{
+ Grove: &nvidiacomv1beta1.GroveSpec{ForceScalingGroup: true},
+ }
+ }),
+ },
+
// Checkpoint rules.
{
name: "v1beta1 valid checkpoint configuration reaches the webhook",
@@ -1897,6 +1927,63 @@ func TestDynamoGraphDeploymentValidator_Validate(t *testing.T) {
"spec.components[1].experimental.failover: Invalid value: null: inter-pod GMS failover cannot be toggled after creation; delete and recreate the DynamoGraphDeployment",
},
},
+ // Grove forceScalingGroup updates.
+ {
+ name: "grove.forceScalingGroup addition is immutable",
+ oldDeployment: newBetaDGDForValidation(),
+ deployment: betaDGDWithWorker(func(worker *nvidiacomv1beta1.DynamoComponentDeploymentSharedSpec) {
+ worker.Experimental = &nvidiacomv1beta1.ExperimentalSpec{
+ Grove: &nvidiacomv1beta1.GroveSpec{ForceScalingGroup: true},
+ }
+ }),
+ wantWebhookErrs: []string{"spec.components[1].experimental.grove.forceScalingGroup: Invalid value: true: cannot be toggled after creation; delete and recreate the DynamoGraphDeployment to change it"},
+ },
+ {
+ name: "grove.forceScalingGroup removal is immutable",
+ oldDeployment: betaDGDWithWorker(func(worker *nvidiacomv1beta1.DynamoComponentDeploymentSharedSpec) {
+ worker.Experimental = &nvidiacomv1beta1.ExperimentalSpec{
+ Grove: &nvidiacomv1beta1.GroveSpec{ForceScalingGroup: true},
+ }
+ }),
+ deployment: newBetaDGDForValidation(),
+ wantWebhookErrs: []string{"spec.components[1].experimental.grove.forceScalingGroup: Invalid value: null: cannot be toggled after creation; delete and recreate the DynamoGraphDeployment to change it"},
+ },
+ {
+ name: "unchanged grove.forceScalingGroup update reaches the webhook",
+ oldDeployment: betaDGDWithWorker(func(worker *nvidiacomv1beta1.DynamoComponentDeploymentSharedSpec) {
+ worker.Experimental = &nvidiacomv1beta1.ExperimentalSpec{
+ Grove: &nvidiacomv1beta1.GroveSpec{ForceScalingGroup: true},
+ }
+ }),
+ deployment: betaDGDWithWorker(func(worker *nvidiacomv1beta1.DynamoComponentDeploymentSharedSpec) {
+ worker.Experimental = &nvidiacomv1beta1.ExperimentalSpec{
+ Grove: &nvidiacomv1beta1.GroveSpec{ForceScalingGroup: true},
+ }
+ }),
+ },
+ {
+ name: "explicit false grove.forceScalingGroup addition reaches the webhook",
+ oldDeployment: betaDGDWithWorker(func(worker *nvidiacomv1beta1.DynamoComponentDeploymentSharedSpec) {
+ worker.Experimental = &nvidiacomv1beta1.ExperimentalSpec{}
+ }),
+ deployment: betaDGDWithWorker(func(worker *nvidiacomv1beta1.DynamoComponentDeploymentSharedSpec) {
+ worker.Experimental = &nvidiacomv1beta1.ExperimentalSpec{
+ Grove: &nvidiacomv1beta1.GroveSpec{ForceScalingGroup: false},
+ }
+ }),
+ },
+ {
+ name: "grove block removal with retained experimental is immutable",
+ oldDeployment: betaDGDWithWorker(func(worker *nvidiacomv1beta1.DynamoComponentDeploymentSharedSpec) {
+ worker.Experimental = &nvidiacomv1beta1.ExperimentalSpec{
+ Grove: &nvidiacomv1beta1.GroveSpec{ForceScalingGroup: true},
+ }
+ }),
+ deployment: betaDGDWithWorker(func(worker *nvidiacomv1beta1.DynamoComponentDeploymentSharedSpec) {
+ worker.Experimental = &nvidiacomv1beta1.ExperimentalSpec{}
+ }),
+ wantWebhookErrs: []string{"spec.components[1].experimental.grove.forceScalingGroup: Invalid value: null: cannot be toggled after creation; delete and recreate the DynamoGraphDeployment to change it"},
+ },
{
name: "inter-pod failover shadow count is immutable",
oldDeployment: alphaDGDForAdmission(func(dgd *nvidiacomv1alpha1.DynamoGraphDeployment) {
diff --git a/deploy/operator/internal/webhook/validation/shared_helpers.go b/deploy/operator/internal/webhook/validation/shared_helpers.go
index d36302e20de9..d170b7fb8933 100644
--- a/deploy/operator/internal/webhook/validation/shared_helpers.go
+++ b/deploy/operator/internal/webhook/validation/shared_helpers.go
@@ -189,6 +189,18 @@ func failoverForExperimental(experimental *nvidiacomv1beta1.ExperimentalSpec) *n
return experimental.Failover
}
+func groveForExperimental(experimental *nvidiacomv1beta1.ExperimentalSpec) *nvidiacomv1beta1.GroveSpec {
+ if experimental == nil {
+ return nil
+ }
+ return experimental.Grove
+}
+
+func forceScalingGroupFor(experimental *nvidiacomv1beta1.ExperimentalSpec) bool {
+ grove := groveForExperimental(experimental)
+ return grove != nil && grove.ForceScalingGroup
+}
+
func effectiveGMSMode(mode nvidiacomv1beta1.GPUMemoryServiceMode) nvidiacomv1beta1.GPUMemoryServiceMode {
if mode == "" {
return nvidiacomv1beta1.GMSModeIntraPod
diff --git a/deploy/operator/internal/webhook/validation/shared_test.go b/deploy/operator/internal/webhook/validation/shared_test.go
index fb1cf341b8f6..36a89fa91695 100644
--- a/deploy/operator/internal/webhook/validation/shared_test.go
+++ b/deploy/operator/internal/webhook/validation/shared_test.go
@@ -363,9 +363,10 @@ func TestValidateExperimentalSpecDoesNotExposePodTemplate(t *testing.T) {
errs := validation.validateExperimentalSpec(
&nvidiacomv1beta1.ExperimentalSpec{GPUMemoryService: gms},
fldPath,
- nvidiacomv1beta1.ComponentTypeWorker,
- corev1.ResourceRequirements{},
- nil,
+ experimentalSpecValidationOptions{
+ componentType: nvidiacomv1beta1.ComponentTypeWorker,
+ grovePathway: true,
+ },
)
assertFieldPaths(t, errs, []string{"spec.components[0].experimental.gpuMemoryService"})
if errs[0].BadValue != "" {
diff --git a/deploy/operator/internal/webhook/validation/shared_v1beta1.go b/deploy/operator/internal/webhook/validation/shared_v1beta1.go
index 91634f0db068..fbe6419a8b98 100644
--- a/deploy/operator/internal/webhook/validation/shared_v1beta1.go
+++ b/deploy/operator/internal/webhook/validation/shared_v1beta1.go
@@ -121,9 +121,12 @@ func (v *sharedValidation) validateDynamoComponentDeploymentSharedSpec(
allErrs = append(allErrs, v.validateExperimentalSpec(
spec.Experimental,
fldPath.Child("experimental"),
- spec.ComponentType,
- dynamo.GetMainContainerResources(spec),
- podTemplateContainers(spec.PodTemplate),
+ experimentalSpecValidationOptions{
+ componentType: spec.ComponentType,
+ resources: dynamo.GetMainContainerResources(spec),
+ containers: podTemplateContainers(spec.PodTemplate),
+ grovePathway: grovePathway,
+ },
)...)
}
@@ -190,22 +193,27 @@ func (v *sharedValidation) validateTopologyConstraint(
return nil
}
+type experimentalSpecValidationOptions struct {
+ componentType nvidiacomv1beta1.ComponentType
+ resources corev1.ResourceRequirements
+ containers []corev1.Container
+ grovePathway bool
+}
+
// validateExperimentalSpec validates experimental. experimental and fldPath must not be nil.
func (v *sharedValidation) validateExperimentalSpec(
experimental *nvidiacomv1beta1.ExperimentalSpec,
fldPath *field.Path,
- componentType nvidiacomv1beta1.ComponentType,
- resources corev1.ResourceRequirements,
- containers []corev1.Container,
+ options experimentalSpecValidationOptions,
) field.ErrorList {
allErrs := field.ErrorList{}
if experimental.GPUMemoryService != nil {
allErrs = append(allErrs, v.validateGPUMemoryServiceSpec(
experimental.GPUMemoryService,
fldPath.Child("gpuMemoryService"),
- componentType,
- resources,
- containers,
+ options.componentType,
+ options.resources,
+ options.containers,
)...)
}
if experimental.Failover != nil {
@@ -213,8 +221,15 @@ func (v *sharedValidation) validateExperimentalSpec(
experimental.Failover,
fldPath.Child("failover"),
experimental.GPUMemoryService,
- componentType,
- resources,
+ options.componentType,
+ options.resources,
+ )...)
+ }
+ if experimental.Grove != nil {
+ allErrs = append(allErrs, v.validateGroveSpec(
+ experimental.Grove,
+ fldPath.Child("grove"),
+ options.grovePathway,
)...)
}
if experimental.Checkpoint != nil {
@@ -344,6 +359,22 @@ func (v *sharedValidation) validateFailoverSpec(
return allErrs
}
+// validateGroveSpec validates grove. grove and fldPath must not be nil.
+// grovePathway is supplied by the owning resource.
+func (v *sharedValidation) validateGroveSpec(
+ grove *nvidiacomv1beta1.GroveSpec,
+ fldPath *field.Path,
+ grovePathway bool,
+) field.ErrorList {
+ if grove.ForceScalingGroup && !grovePathway {
+ return field.ErrorList{field.Forbidden(
+ fldPath.Child("forceScalingGroup"),
+ "is currently supported only for Grove-backed DynamoGraphDeployment components",
+ )}
+ }
+ return nil
+}
+
// validateComponentCheckpointConfig validates checkpoint. checkpoint and fldPath must not be nil.
// gms may be nil because checkpoint validates that sibling relationship.
func (v *sharedValidation) validateComponentCheckpointConfig(
@@ -459,6 +490,13 @@ func (v *sharedValidation) validateDynamoComponentDeploymentSharedSpecUpdate(
fmt.Sprintf("inter-pod GMS failover cannot be toggled after creation; delete and recreate the %s", ownerKind.Kind),
))
}
+ if forceScalingGroupFor(oldComponent.Experimental) {
+ allErrs = append(allErrs, field.Invalid(
+ fldPath.Child("experimental", "grove", "forceScalingGroup"),
+ nil,
+ fmt.Sprintf("cannot be toggled after creation; delete and recreate the %s to change it", ownerKind.Kind),
+ ))
+ }
}
// Ratchet legacy image absence or an unchanged legacy tuple, but reject a newly invalid tuple.
@@ -551,5 +589,41 @@ func (v *sharedValidation) validateExperimentalSpecUpdate(
fmt.Sprintf("is immutable for inter-pod GMS failover; delete and recreate the %s to change it", options.ownerKind.Kind),
))
}
+
+ oldGrove := groveForExperimental(oldExperimental)
+ if newExperimental.Grove != nil {
+ allErrs = append(allErrs, v.validateGroveSpecUpdate(
+ newExperimental.Grove,
+ oldGrove,
+ fldPath.Child("grove"),
+ options.ownerKind,
+ )...)
+ } else if oldGrove != nil && oldGrove.ForceScalingGroup {
+ allErrs = append(allErrs, field.Invalid(
+ fldPath.Child("grove", "forceScalingGroup"),
+ nil,
+ fmt.Sprintf("cannot be toggled after creation; delete and recreate the %s to change it", options.ownerKind.Kind),
+ ))
+ }
return allErrs
}
+
+// validateGroveSpecUpdate validates a grove update. newGrove and fldPath must
+// not be nil; oldGrove may be nil for an addition. false and omitted both
+// mean automatic selection, so only the effective opt-in is immutable.
+func (v *sharedValidation) validateGroveSpecUpdate(
+ newGrove *nvidiacomv1beta1.GroveSpec,
+ oldGrove *nvidiacomv1beta1.GroveSpec,
+ fldPath *field.Path,
+ ownerKind schema.GroupKind,
+) field.ErrorList {
+ oldForced := oldGrove != nil && oldGrove.ForceScalingGroup
+ if newGrove.ForceScalingGroup == oldForced {
+ return nil
+ }
+ return field.ErrorList{field.Invalid(
+ fldPath.Child("forceScalingGroup"),
+ newGrove.ForceScalingGroup,
+ fmt.Sprintf("cannot be toggled after creation; delete and recreate the %s to change it", ownerKind.Kind),
+ )}
+}
diff --git a/docs/fern/pages/reference/kubernetes-api/additional-resources/api-reference-k8s.md b/docs/fern/pages/reference/kubernetes-api/additional-resources/api-reference-k8s.md
index 6aba62617c59..60f11b7cf01e 100644
--- a/docs/fern/pages/reference/kubernetes-api/additional-resources/api-reference-k8s.md
+++ b/docs/fern/pages/reference/kubernetes-api/additional-resources/api-reference-k8s.md
@@ -2127,7 +2127,7 @@ _Appears in:_
| `globalDynamoNamespace` _boolean_ | globalDynamoNamespace places the component in the global Dynamo
namespace rather than the per-deployment namespace derived from the
DGD name. | | Optional: \{\}
|
| `podTemplate` _[PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#podtemplatespec-v1-core)_ | podTemplate defines the component's Pod configuration. New components must
include a container named "main" with a non-empty image. Existing components
created without a podTemplate may remain unchanged. The operator merges
defaults into the main container.
For DGD components whose main image tag is not a Dynamo semantic version,
set runtimeVersionOverride explicitly.
All other containers are user-managed sidecars and must specify their
required fields, including image. | | Optional: \{\}
|
| `replicas` _integer_ | replicas is the desired number of Pods for this component. When
`scalingAdapter` is set on this component, this field is managed by
the DynamoGraphDeploymentScalingAdapter and should not be modified
directly. | | Minimum: 0
Optional: \{\}
|
-| `minAvailable` _integer_ | minAvailable maps to Grove PodClique minAvailable for single-node and
Grove PodCliqueScalingGroup minAvailable for multi-node components.
This field determines 1) the minimum number of replicas guaranteed to be
gang-scheduled, and 2) when violating minAvailable replicas triggers gang
termination.
For Grove-backed DynamoGraphDeployment components, minAvailable defaults to
1 when omitted and is immutable after creation. Positive replica counts must
be greater than or equal to minAvailable. Replicas may be scaled to 0 as a
special scale-to-zero state; minAvailable remains configured but is not
enforced again until replicas is scaled back to a positive value.
For non-Grove deployments, setting this field will result in a validation error. | | Minimum: 1
Optional: \{\}
|
+| `minAvailable` _integer_ | minAvailable maps to Grove PodCliqueScalingGroup minAvailable for
components rendered as a scaling group (multi-node, inter-pod GMS, or
`experimental.grove.forceScalingGroup`; see `UsesPCSG`) and to Grove
PodClique minAvailable for all other single-node components.
This field determines 1) the minimum number of replicas guaranteed to be
gang-scheduled, and 2) when violating minAvailable replicas triggers gang
termination.
For Grove-backed DynamoGraphDeployment components, minAvailable defaults to
1 when omitted and is immutable after creation. Positive replica counts must
be greater than or equal to minAvailable. Replicas may be scaled to 0 as a
special scale-to-zero state; minAvailable remains configured but is not
enforced again until replicas is scaled back to a positive value.
For non-Grove deployments, setting this field will result in a validation error. | | Minimum: 1
Optional: \{\}
|
| `multinode` _[MultinodeSpec](#multinodespec)_ | multinode configures multinode components. | | Optional: \{\}
|
| `sharedMemorySize` _[Quantity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#quantity-resource-api)_ | sharedMemorySize controls the size of the tmpfs mounted at `/dev/shm`.
`nil` selects the operator default (8Gi), a positive quantity sets a
custom size, and `"0"` disables the shared-memory volume entirely.
Simpler replacement for v1alpha1's `SharedMemorySpec` struct with its
`disabled bool` + `size Quantity` pattern. | | Optional: \{\}
|
| `modelRef` _[ModelReference](#modelreference)_ | modelRef references a model served by this component. When specified,
a headless service is created for endpoint discovery. | | Optional: \{\}
|
@@ -2159,7 +2159,7 @@ _Appears in:_
| `globalDynamoNamespace` _boolean_ | globalDynamoNamespace places the component in the global Dynamo
namespace rather than the per-deployment namespace derived from the
DGD name. | | Optional: \{\}
|
| `podTemplate` _[PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#podtemplatespec-v1-core)_ | podTemplate defines the component's Pod configuration. New components must
include a container named "main" with a non-empty image. Existing components
created without a podTemplate may remain unchanged. The operator merges
defaults into the main container.
For DGD components whose main image tag is not a Dynamo semantic version,
set runtimeVersionOverride explicitly.
All other containers are user-managed sidecars and must specify their
required fields, including image. | | Optional: \{\}
|
| `replicas` _integer_ | replicas is the desired number of Pods for this component. When
`scalingAdapter` is set on this component, this field is managed by
the DynamoGraphDeploymentScalingAdapter and should not be modified
directly. | | Minimum: 0
Optional: \{\}
|
-| `minAvailable` _integer_ | minAvailable maps to Grove PodClique minAvailable for single-node and
Grove PodCliqueScalingGroup minAvailable for multi-node components.
This field determines 1) the minimum number of replicas guaranteed to be
gang-scheduled, and 2) when violating minAvailable replicas triggers gang
termination.
For Grove-backed DynamoGraphDeployment components, minAvailable defaults to
1 when omitted and is immutable after creation. Positive replica counts must
be greater than or equal to minAvailable. Replicas may be scaled to 0 as a
special scale-to-zero state; minAvailable remains configured but is not
enforced again until replicas is scaled back to a positive value.
For non-Grove deployments, setting this field will result in a validation error. | | Minimum: 1
Optional: \{\}
|
+| `minAvailable` _integer_ | minAvailable maps to Grove PodCliqueScalingGroup minAvailable for
components rendered as a scaling group (multi-node, inter-pod GMS, or
`experimental.grove.forceScalingGroup`; see `UsesPCSG`) and to Grove
PodClique minAvailable for all other single-node components.
This field determines 1) the minimum number of replicas guaranteed to be
gang-scheduled, and 2) when violating minAvailable replicas triggers gang
termination.
For Grove-backed DynamoGraphDeployment components, minAvailable defaults to
1 when omitted and is immutable after creation. Positive replica counts must
be greater than or equal to minAvailable. Replicas may be scaled to 0 as a
special scale-to-zero state; minAvailable remains configured but is not
enforced again until replicas is scaled back to a positive value.
For non-Grove deployments, setting this field will result in a validation error. | | Minimum: 1
Optional: \{\}
|
| `multinode` _[MultinodeSpec](#multinodespec)_ | multinode configures multinode components. | | Optional: \{\}
|
| `sharedMemorySize` _[Quantity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#quantity-resource-api)_ | sharedMemorySize controls the size of the tmpfs mounted at `/dev/shm`.
`nil` selects the operator default (8Gi), a positive quantity sets a
custom size, and `"0"` disables the shared-memory volume entirely.
Simpler replacement for v1alpha1's `SharedMemorySpec` struct with its
`disabled bool` + `size Quantity` pattern. | | Optional: \{\}
|
| `modelRef` _[ModelReference](#modelreference)_ | modelRef references a model served by this component. When specified,
a headless service is created for endpoint discovery. | | Optional: \{\}
|
@@ -2469,6 +2469,7 @@ _Appears in:_
| --- | --- | --- | --- |
| `gpuMemoryService` _[GPUMemoryServiceSpec](#gpumemoryservicespec)_ | gpuMemoryService configures the GPU Memory Service (GMS). When set, GPU
access for GMS clients is managed via DRA. | | Optional: \{\}
|
| `failover` _[FailoverSpec](#failoverspec)_ | failover configures active-passive GPU failover for this component.
Requires `gpuMemoryService` to also be set, and `failover.mode` must
match `gpuMemoryService.mode` (enforced by the validation webhook). | | Optional: \{\}
|
+| `grove` _[GroveSpec](#grovespec)_ | grove groups Grove-specific rendering options. | | Optional: \{\}
|
| `checkpoint` _[ComponentCheckpointConfig](#componentcheckpointconfig)_ | checkpoint configures container-image snapshotting and restore for
this component. Set `checkpoint.enabled: true` to opt in. Without
checkpointRef, the DGD controller creates a DGD-scoped DynamoCheckpoint
CR and later restores pods in the same DGD generation from that
checkpoint. With checkpointRef, the DGD restores from that existing
checkpoint instead. The user-facing shape of this field is still settling,
which is why it lives under `experimental` in v1beta1 instead of at the
top level. | | Optional: \{\}
|
@@ -2602,6 +2603,22 @@ _Appears in:_
| `mi300` | |
+#### GroveSpec
+
+
+
+GroveSpec groups experimental Grove-specific rendering options.
+
+
+
+_Appears in:_
+- [ExperimentalSpec](#experimentalspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `forceScalingGroup` _boolean_ | forceScalingGroup opts a single-node component into rendering as a
PodCliqueScalingGroup with one single-pod PodClique per replica.
Scaling changes the scaling-group replica count. The first
`minAvailable` replicas join the deployment's base PodGang together
with its other base workloads; each replica beyond `minAvailable`
gets its own PodGang, gang-scheduled separately from the rest of the
deployment. `false` or omitted means automatic selection (multi-node
and inter-pod GMS components use a scaling group, other single-node
components a standalone PodClique), not "force PodClique".
Immutable after creation. | | Optional: \{\}
|
+
+
#### HardwareSpec