Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ type DynamoComponentDeploymentSharedSpec struct {
// main image. DGD admission requires it when spec.extraPodSpec.mainContainer.image has no parseable
// semantic-version tag; controller-generated DCDs may omit it. Set it also when the parsed tag is
// not the Dynamo runtime version. Use the canonical MAJOR.MINOR.PATCH value, for example "1.4.0".
// It does not change the image or rendered Pod, and changing only this field does not trigger a rollout.
// Keep this value aligned with the main image and update it only when the image's runtime
// compatibility version changes. Changing it may alter runtime-gated Pod rendering and trigger
// a rollout; it does not change the image itself.
// +kubebuilder:validation:Pattern=`^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$`
// +optional
RuntimeVersionOverride string `json:"runtimeVersionOverride,omitempty"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ type DynamoComponentDeploymentSharedSpec struct {
// main image. DGD admission requires it when spec.podTemplate.spec.containers[name=main].image has
// no parseable semantic-version tag; controller-generated DCDs may omit it. Set it also when the
// parsed tag is not the Dynamo runtime version. Use the canonical MAJOR.MINOR.PATCH value, for
// example "1.4.0". It does not change the image or rendered Pod, and changing only this field does
// not trigger a rollout.
// example "1.4.0". Keep this value aligned with the main image and update it only when the image's
// runtime compatibility version changes. Changing it may alter runtime-gated Pod rendering and
// trigger a rollout; it does not change the image itself.
// +kubebuilder:validation:Pattern=`^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$`
// +optional
RuntimeVersionOverride string `json:"runtimeVersionOverride,omitempty"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11455,7 +11455,9 @@ spec:
main image. DGD admission requires it when spec.extraPodSpec.mainContainer.image has no parseable
semantic-version tag; controller-generated DCDs may omit it. Set it also when the parsed tag is
not the Dynamo runtime version. Use the canonical MAJOR.MINOR.PATCH value, for example "1.4.0".
It does not change the image or rendered Pod, and changing only this field does not trigger a rollout.
Keep this value aligned with the main image and update it only when the image's runtime
compatibility version changes. Changing it may alter runtime-gated Pod rendering and trigger
a rollout; it does not change the image itself.
pattern: ^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$
type: string
scalingAdapter:
Expand Down Expand Up @@ -20419,8 +20421,9 @@ spec:
main image. DGD admission requires it when spec.podTemplate.spec.containers[name=main].image has
no parseable semantic-version tag; controller-generated DCDs may omit it. Set it also when the
parsed tag is not the Dynamo runtime version. Use the canonical MAJOR.MINOR.PATCH value, for
example "1.4.0". It does not change the image or rendered Pod, and changing only this field does
not trigger a rollout.
example "1.4.0". Keep this value aligned with the main image and update it only when the image's
runtime compatibility version changes. Changing it may alter runtime-gated Pod rendering and
trigger a rollout; it does not change the image itself.
pattern: ^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$
type: string
scalingAdapter:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11755,7 +11755,9 @@ spec:
main image. DGD admission requires it when spec.extraPodSpec.mainContainer.image has no parseable
semantic-version tag; controller-generated DCDs may omit it. Set it also when the parsed tag is
not the Dynamo runtime version. Use the canonical MAJOR.MINOR.PATCH value, for example "1.4.0".
It does not change the image or rendered Pod, and changing only this field does not trigger a rollout.
Keep this value aligned with the main image and update it only when the image's runtime
compatibility version changes. Changing it may alter runtime-gated Pod rendering and trigger
a rollout; it does not change the image itself.
Comment thread
xianlubird marked this conversation as resolved.
Resolved
pattern: ^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$
type: string
scalingAdapter:
Expand Down Expand Up @@ -20877,8 +20879,9 @@ spec:
main image. DGD admission requires it when spec.podTemplate.spec.containers[name=main].image has
no parseable semantic-version tag; controller-generated DCDs may omit it. Set it also when the
parsed tag is not the Dynamo runtime version. Use the canonical MAJOR.MINOR.PATCH value, for
example "1.4.0". It does not change the image or rendered Pod, and changing only this field does
not trigger a rollout.
example "1.4.0". Keep this value aligned with the main image and update it only when the image's
runtime compatibility version changes. Changing it may alter runtime-gated Pod rendering and
trigger a rollout; it does not change the image itself.
pattern: ^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$
type: string
scalingAdapter:
Expand Down
2 changes: 2 additions & 0 deletions deploy/operator/internal/dynamo/component_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1"
commonconsts "github.com/ai-dynamo/dynamo/deploy/operator/internal/consts"
controller_common "github.com/ai-dynamo/dynamo/deploy/operator/internal/controller_common"
runtimefeatures "github.com/ai-dynamo/dynamo/deploy/operator/internal/features/runtime"
corev1 "k8s.io/api/core/v1"
"k8s.io/utils/ptr"
)
Expand Down Expand Up @@ -67,6 +68,7 @@ type ComponentContext struct {
Discovery DiscoveryContext
EPPConfig *v1beta1.EPPConfig
WorkerHashSuffix string
RuntimeProfile runtimefeatures.RuntimeProfile
}

func (b *BaseComponentDefaults) GetBaseContainer(context ComponentContext) (corev1.Container, error) {
Expand Down
1 change: 1 addition & 0 deletions deploy/operator/internal/dynamo/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -1880,6 +1880,7 @@ func generateComponentContext(component *v1beta1.DynamoComponentDeploymentShared
DynamoNamespace: dynamoNamespace,
EPPConfig: component.EPPConfig,
WorkerHashSuffix: workerHashSuffix,
RuntimeProfile: runtimeProfileForComponent(component),
Comment thread
xianlubird marked this conversation as resolved.
Resolved
}
return componentContext
}
Expand Down
21 changes: 21 additions & 0 deletions deploy/operator/internal/dynamo/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,7 @@ func TestGenerateComponentContext(t *testing.T) {
expectedComponentType string
expectedParentDGDName string
expectedParentDGDNamespace string
expectedCanaryHealthChecks bool
}{
{
name: "namespace-scoped operator: computes correct dynamo namespace",
Expand Down Expand Up @@ -1426,6 +1427,25 @@ func TestGenerateComponentContext(t *testing.T) {
expectedParentDGDName: "shared-frontend",
expectedParentDGDNamespace: "production",
},
{
name: "runtime profile is resolved for the component",
component: &v1alpha1.DynamoComponentDeploymentSharedSpec{
ComponentType: commonconsts.ComponentTypeWorker,
RuntimeVersionOverride: "1.4.0",
ExtraPodSpec: &v1alpha1.ExtraPodSpec{
MainContainer: &corev1.Container{Image: "registry.example/runtime:custom"},
},
},
parentGraphDeploymentName: "runtime-profile",
namespace: "default",
numberOfNodes: 1,
discoveryBackend: configv1alpha1.DiscoveryBackendKubernetes,
expectedDynamoNamespace: "default-runtime-profile",
expectedComponentType: commonconsts.ComponentTypeWorker,
expectedParentDGDName: "runtime-profile",
expectedParentDGDNamespace: "default",
expectedCanaryHealthChecks: true,
},
Comment thread
xianlubird marked this conversation as resolved.
Resolved
{
name: "nil dynamoNamespace field still computes correctly",
component: &v1alpha1.DynamoComponentDeploymentSharedSpec{
Expand Down Expand Up @@ -1474,6 +1494,7 @@ func TestGenerateComponentContext(t *testing.T) {
assert.Equal(t, tt.expectedParentDGDNamespace, ctx.ParentGraphDeploymentNamespace)
assert.Equal(t, tt.numberOfNodes, ctx.numberOfNodes)
assert.Equal(t, tt.discoveryBackend, ctx.Discovery.Backend)
assert.Equal(t, tt.expectedCanaryHealthChecks, ctx.RuntimeProfile.CanaryHealthChecks)
})
}
}
Expand Down
31 changes: 24 additions & 7 deletions deploy/operator/internal/dynamo/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/ai-dynamo/dynamo/deploy/operator/api/v1alpha1"
"github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1"
runtimefeatures "github.com/ai-dynamo/dynamo/deploy/operator/internal/features/runtime"
)

const dgdWorkerHashPlaceholderValue = "worker-hash-placeholder"
Expand Down Expand Up @@ -60,9 +61,10 @@ func ComputeDGDWorkersSpecHash(dgd *v1beta1.DynamoGraphDeployment) (string, erro
}

type workerTemplate struct {
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Spec v1beta1.DynamoComponentDeploymentSpec `json:"spec"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
RuntimeProfile *runtimefeatures.RuntimeProfile `json:"runtimeProfile,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs thought why it is correct to add the gates to the hash. And further, what it means when gates changes, or gates are introduced in a version, but are false (then the hash should not change?).

And we need a plan for when gates become true, whether we tolerate automatic rollouts, in general or only by opt-in.

Spec v1beta1.DynamoComponentDeploymentSpec `json:"spec"`
}

workerDCDs := make(map[string]workerTemplate, len(dcds))
Expand All @@ -75,10 +77,15 @@ func ComputeDGDWorkersSpecHash(dgd *v1beta1.DynamoGraphDeployment) (string, erro
if _, exists := workerDCDs[componentName]; exists {
return "", fmt.Errorf("duplicate generated worker DCD component name %q", componentName)
}

// Hash effective gate decisions alongside the generated worker template.
profile := runtimeProfileForComponent(&dcd.Spec.DynamoComponentDeploymentSharedSpec)

workerDCDs[componentName] = workerTemplate{
Labels: GetDCDKubeLabels(dcd),
Annotations: GetDCDKubeAnnotations(dcd),
Spec: workerHashSpec(dcd),
Labels: GetDCDKubeLabels(dcd),
Annotations: GetDCDKubeAnnotations(dcd),
RuntimeProfile: nonEmptyRuntimeProfile(profile),
Spec: workerHashSpec(dcd),
}
}
}
Expand All @@ -101,8 +108,18 @@ func workerHashSpec(dcd *v1beta1.DynamoComponentDeployment) v1beta1.DynamoCompon
spec.MinAvailable = nil
spec.ScalingAdapter = nil

// RuntimeVersionOverride has no rendered Pod effect yet.
// Hash the resolved rendering profile rather than this raw optional input.
spec.RuntimeVersionOverride = ""

return *spec
}

func nonEmptyRuntimeProfile(profile runtimefeatures.RuntimeProfile) *runtimefeatures.RuntimeProfile {
// Preserve the existing hash shape until at least one runtime gate is enabled.
if profile.IsEmpty() {
return nil
}

// Include enabled decisions in the canonical worker hash input.
return &profile
}
91 changes: 87 additions & 4 deletions deploy/operator/internal/dynamo/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import (
"k8s.io/utils/ptr"
)

const testRuntimeVersion15 = "1.5.0"
Comment thread
xianlubird marked this conversation as resolved.
Resolved

func baseDGD(services map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec) *v1alpha1.DynamoGraphDeployment {
return &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
Expand Down Expand Up @@ -90,6 +92,29 @@ func TestComputeLegacyAlphaDGDWorkersSpecHash_MatchesV1Alpha1Hash(t *testing.T)
assert.NotEqual(t, mustComputeBetaDGDWorkersSpecHash(t, beta), legacyHash)
}

func TestComputeLegacyAlphaDGDWorkersSpecHash_IgnoresRuntimeProfile(t *testing.T) {
t.Log("construct a legacy worker with an enabled runtime profile")
alpha := baseDGD(map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"worker": {
ComponentType: commonconsts.ComponentTypeWorker,
ExtraPodSpec: &v1alpha1.ExtraPodSpec{
MainContainer: &corev1.Container{Image: "registry.example/runtime:custom"},
},
RuntimeVersionOverride: "1.4.0",
},
})

t.Log("compute the frozen alpha hash before and after an equivalent profile change")
baseHash, err := ComputeLegacyAlphaDGDWorkersSpecHash(betaDGD(t, alpha))
assert.NoError(t, err)
alpha.Spec.Services["worker"].RuntimeVersionOverride = testRuntimeVersion15
changedHash, err := ComputeLegacyAlphaDGDWorkersSpecHash(betaDGD(t, alpha))
assert.NoError(t, err)

t.Log("verify runtime profiles do not alter the legacy hash contract")
assert.Equal(t, baseHash, changedHash)
}
Comment thread
xianlubird marked this conversation as resolved.
Resolved

func TestComputeLegacyAlphaDGDWorkersSpecHash_RecoversNameOnlyMainContainerHash(t *testing.T) {
alpha := baseDGD(map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"worker": {
Expand Down Expand Up @@ -296,16 +321,74 @@ func TestComputeBetaDGDWorkersSpecHash_IgnoresNonRolloutFields(t *testing.T) {
assert.Equal(t, baseHash, mustComputeBetaDGDWorkersSpecHash(t, betaDGD(t, disabledScalingAdapter)))
}

func TestComputeBetaDGDWorkersSpecHash_IgnoresRuntimeVersionOverride(t *testing.T) {
func TestComputeBetaDGDWorkersSpecHash_KeepsPreGateRuntimeProfileEmpty(t *testing.T) {
t.Log("compute the hash with an unresolved legacy runtime")
base := betaDGD(t, baseDGD(map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"worker": {ComponentType: commonconsts.ComponentTypeWorker},
}))
baseHash := mustComputeBetaDGDWorkersSpecHash(t, base)

withOverride := base.DeepCopy()
withOverride.Spec.Components[0].RuntimeVersionOverride = "1.4.0"
t.Log("resolve an explicit runtime below every feature threshold")
runtime13 := base.DeepCopy()
runtime13.Spec.Components[0].RuntimeVersionOverride = "1.3.0"

t.Log("verify both inputs retain the same empty rendering profile")
assert.Equal(t, baseHash, mustComputeBetaDGDWorkersSpecHash(t, runtime13))
}

func TestComputeBetaDGDWorkersSpecHash_UsesCanonicalResolvedRuntimeProfile(t *testing.T) {
t.Log("compute the profile hash from a semantic image tag")
base := betaDGD(t, baseDGD(map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"worker": {ComponentType: commonconsts.ComponentTypeWorker},
}))
base.Spec.Components[0].PodTemplate = &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: commonconsts.MainContainerName,
Image: "nvcr.io/nvidia/ai-dynamo/runtime:v1.5.0-cuda13",
}},
},
}
implicitHash := mustComputeBetaDGDWorkersSpecHash(t, base)

t.Log("declare the equivalent compatibility version explicitly")
explicit := base.DeepCopy()
explicit.Spec.Components[0].RuntimeVersionOverride = testRuntimeVersion15

t.Log("verify equivalent resolved profiles hash identically")
assert.Equal(t, implicitHash, mustComputeBetaDGDWorkersSpecHash(t, explicit))
}

func TestComputeBetaDGDWorkersSpecHash_ChangesWithRuntimeProfile(t *testing.T) {
t.Log("compute the hash for a runtime below the canary gate")
base := betaDGD(t, baseDGD(map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"worker": {ComponentType: commonconsts.ComponentTypeWorker},
}))
base.Spec.Components[0].PodTemplate = &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: commonconsts.MainContainerName,
Image: "registry.example/runtime:custom",
}},
},
}
base.Spec.Components[0].RuntimeVersionOverride = "1.3.0"
disabledProfileHash := mustComputeBetaDGDWorkersSpecHash(t, base)

t.Log("cross the canary gate threshold")
versioned := base.DeepCopy()
versioned.Spec.Components[0].RuntimeVersionOverride = "1.4.0"
enabledProfileHash := mustComputeBetaDGDWorkersSpecHash(t, versioned)

t.Log("verify a changed rendering profile produces a new worker generation")
assert.NotEqual(t, disabledProfileHash, enabledProfileHash)

t.Log("move to a newer runtime with the same effective gates")
newerSameProfile := base.DeepCopy()
newerSameProfile.Spec.Components[0].RuntimeVersionOverride = "2.0.0"

assert.Equal(t, baseHash, mustComputeBetaDGDWorkersSpecHash(t, withOverride))
t.Log("verify an equivalent rendering profile avoids an unnecessary rollout")
assert.Equal(t, enabledProfileHash, mustComputeBetaDGDWorkersSpecHash(t, newerSameProfile))
}

func TestComputeBetaDGDWorkersSpecHash_TracksPreservedAlphaResourceMetadata(t *testing.T) {
Expand Down
35 changes: 35 additions & 0 deletions deploy/operator/internal/dynamo/runtime_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package dynamo

import (
"github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1"
runtimefeatures "github.com/ai-dynamo/dynamo/deploy/operator/internal/features/runtime"
"github.com/ai-dynamo/dynamo/deploy/operator/internal/runtimeversion"
)

// runtimeProfileForComponent resolves and evaluates the component's runtime gates.
func runtimeProfileForComponent(component *v1beta1.DynamoComponentDeploymentSharedSpec) runtimefeatures.RuntimeProfile {
// Keep legacy behavior when no component inputs are available.
if component == nil {
return runtimefeatures.RuntimeProfile{}
}

// Read the effective main image used by runtime version resolution.
image := ""
if main := GetMainContainer(component); main != nil {
image = main.Image
}

// Unresolvable legacy images keep every runtime-dependent default disabled.
version, err := runtimeversion.Resolve(image, component.RuntimeVersionOverride)
if err != nil {
return runtimefeatures.RuntimeProfile{}
}

// Evaluate the centralized gate set for the resolved compatibility version.
return runtimefeatures.ProfileForVersion(&version)
}
Loading
Loading