Skip to content

fix(operator): bind workloads to verified checkpoints - #12873

Open
galletas1712 wants to merge 1 commit into
fix/operator-automatic-checkpoint-provenancefrom
fix/operator-checkpoint-workload-binding
Open

fix(operator): bind workloads to verified checkpoints#12873
galletas1712 wants to merge 1 commit into
fix/operator-automatic-checkpoint-provenancefrom
fix/operator-checkpoint-workload-binding

Conversation

@galletas1712

@galletas1712 galletas1712 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

This is the seventh layer of the eight-PR stack for DEP #12671. It durably binds generated workloads to the exact automatic checkpoint verified by #12872.

A checkpoint can be deleted, replaced under the same name, or changed after a workload is rendered. Name-only lookup would let that workload restore from a different artifact.

The binding format is:

v1/<checkpoint UID>/<generation>/<canonical provenance SHA-256>

UID detects same-name replacement, generation detects specification changes, and the digest detects metadata-only provenance changes such as artifact version, checkpoint ID, deletion policy, or owner changes.

flowchart LR
    C["Verified DynamoCheckpoint"] --> B["Versioned binding"]
    B --> D["DynamoComponentDeployment"]
    B --> G["Grove engine clique"]
    D --> P["Restore-target Pod"]
    G --> P
    P --> V["Admission re-reads and verifies checkpoint"]
Loading

DCD metadata and Pod-template copies must both exist and agree. Grove stores a checkpoint map at the PodCliqueSet and a singular binding only on the relevant engine clique; GMS and unrelated cliques are not stamped. Bound, already-shaped restore Pods are revalidated before admission returns, while checkpoint source Pods and ordinary unbound Pods retain existing behavior.

This binding depends on #12902 preserving explicit checkpoint identity before #12872 verifies and hashes its provenance; otherwise the durable digest could bind false normalized TP1 metadata for a real TP2 capture.

Missing, malformed, legacy, divergent, replaced, or stale bindings fail closed.

Validation

  • DCD metadata/template consistency and atomic-sync tests passed
  • Grove PCS/per-engine-clique binding and rolling-update tests passed
  • already-shaped Pod admission covers missing, unmarked, replacement, stale, matching, source, and ordinary-unbound cases
  • metadata-only mutation tests cover artifact version, checkpoint ID, marker, owner, deletion policy, and capture spec
  • exact-commit checkpoint, controller, controller-common, and mutation-webhook suites passed
  • envtest, go vet, DCO, signature, and diff-hygiene checks passed

Final eight-branch chain validation

  • Operator checkpoint/controller/webhook validation passed with go test ./internal/checkpoint ./internal/controller ./internal/webhook/validation -count=1.
  • Operator lint passed with make lint, invoking golangci-lint v1.64.8.
  • Snapshot validation passed with make lint, go test ./internal/criu -count=1, go test -race ./internal/criu -count=1, go test ./... -count=1, and go vet ./... from deploy/snapshot.
  • Python formatting/lint hooks passed with repository-pinned isort 5.12.0, Black 23.1.0, flake8 7.3.0, and Ruff 0.5.2.
  • Independent reviews approved the final local chain.
  • Full live AKS Snapshot-backed failover E2E for the supported TP=PP=DP=1 profile: PASS.

Live AKS TP2 identity validation

At exact eight-branch composite d209f77f91911a4267dea203490a0e410b9e1b1e:

  • A namespace-scoped AKS operator upgrade used the corrected exact composite image index sha256:fba2f709673f7949b3da463a577e18522a11bd35ca08634c1ff06329529077aa (amd64 manifest sha256:dc2d076270079a7e4f65e04eacda6c7227a4870e1c1e75fc4c8d38de6b552b5b); CRDs were untouched.
  • A fresh ordinary TP2 source became Ready with ranks 0/1, two DRA GPUs, and inference HTTP 200.
  • Checkpoint checkpoint-5d2f7c0d202210efee4455fa50f3727d (UID 89acc792-c13d-4302-9dab-bb4e8e143eac) became Ready. Its live identity was exactly model Qwen/Qwen3-0.6B, backend vllm, TP=2, and PP=1; custom extra metadata was preserved, while spoofed reserved dgdUID, component, and checkpointID values were overwritten correctly.
  • GMS V1 artifacts device-0 and device-1 each contained 24 allocations totaling 610,271,232 bytes.
  • Two fresh restores from immutable binding v1/89acc792-c13d-4302-9dab-bb4e8e143eac/1/942c43260c8204ad6867ebea038ea31cc37b390d406fbf1ee1f6d9cd700b1c73 both reached RestoreSucceeded, ranks 0/1, two GPUs/loaders, and inference HTTP 200; restore totals were approximately 21.53s and 20.95s.
  • No CRIU, socket, pagemap, or TCP regressions were observed. The existing full-failover TP1 DGD remained Ready and continued serving inference with HTTP 200.

Scope: This proves ordinary TP2 Snapshot capture/restore plus corrected identity, provenance, and immutable binding. TP2 automatic failover remains unsupported and is not claimed; Snapshot-backed automatic failover remains limited to TP=PP=DP=1.

Full CI is not claimed.

Stack

GitHub Stack: #12903

Order PR Change
1 #12887 Remap clone-unsafe sockets during restore
2 #12902 Preserve automatic checkpoint identity
3 #12869 Elect the failover owner before worker publication
4 #12870 Support a second IntraPod shadow
5 #12871 Retain restored engines paused
6 #12872 Verify automatic checkpoint provenance
7 #12873 Bind workloads to verified checkpoints
8 #12874 Enable Snapshot-backed IntraPod failover

@datadog-official

datadog-official Bot commented Aug 8, 2026

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 1 Pipeline job failed

Pre Merge | pre-merge-status-check   View in Datadog   GitHub Actions

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: b941527 | Docs | Datadog PR Page | Give us feedback!

@galletas1712
galletas1712 requested review from a team as code owners August 8, 2026 04:36
@galletas1712
galletas1712 force-pushed the fix/operator-checkpoint-workload-binding branch from b35cb65 to a57a93a Compare August 8, 2026 04:36

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 6 potential issues.

Open in Devin Review

Comment on lines +147 to +153
actual := &nvidiacomv1alpha1.DynamoCheckpoint{}
if err := r.reader.Get(ctx, types.NamespacedName{
Namespace: namespace,
Name: info.CheckpointName,
}, actual); err != nil {
return fmt.Errorf("get checkpoint %q: %w", info.CheckpointName, err)
}

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.

🟡 First deployment of a Grove workload can fail because the just-created checkpoint is looked up before it is visible

The automatic checkpoint is re-read from the controller's local cache (r.reader.Get at deploy/operator/internal/controller/dgd_grove_workload_renderer.go:148-153) in the same pass that just created it, so the very first deployment attempt usually fails with a "not found" error instead of proceeding.
Impact: New checkpoint-enabled Grove deployments report a spurious failure and only converge on a later retry, delaying rollout and emitting misleading errors.

Read-your-own-write through the informer cache

In one DGD reconcile pass dgd_shared_resources_reconciler.go:91 runs dgdCheckpointsReconciler.Reconcile, which calls checkpoint.CreateOrGetAutoCheckpoint and creates the DynamoCheckpoint (deploy/operator/internal/controller/dgd_checkpoints_reconciler.go:169-194), then computes info.AutoBinding from the object returned by the create call. Immediately afterwards, groveProgram.Reconcile (deploy/operator/internal/controller/dgd_grove_program.go:104-116) invokes groveWorkloadRenderer.Render, which calls verifyAutomaticCheckpointBindings and Gets the same checkpoint through r.reader — the manager's cached client (newGroveWorkloadRenderer(kubeClient, ...) at deploy/operator/internal/controller/dgd_grove_workloads_reconciler.go:57-63).

The informer cache has almost certainly not observed the create yet, so Get returns NotFound and the renderer returns get checkpoint %q: not found, aborting the whole Grove reconcile before the PodCliqueSet is created or updated. deploy/operator/internal/controller/AGENTS.md states: "After a successful write, either continue with the object returned by the client or wait for its watch event" and "do not turn expected informer lag into a terminal failure".

A fix would be to verify against the checkpoint object already returned by CreateOrGetAutoCheckpoint (which is what produced AutoBinding), or to treat a cache miss / mismatch for a checkpoint created in this pass as pending and requeue rather than as an error.

Prompt for agents
In deploy/operator/internal/controller/dgd_grove_workload_renderer.go, verifyAutomaticCheckpointBindings re-reads each automatic DynamoCheckpoint through the manager's cached reader. In the same reconcile pass, dgdCheckpointsReconciler.Reconcile has just created that checkpoint (see deploy/operator/internal/controller/dgd_checkpoints_reconciler.go where AutoBinding is computed from the object returned by CreateOrGetAutoCheckpoint), so the informer cache typically has not observed it yet and the Get fails with NotFound, which aborts the entire Grove render and PodCliqueSet reconciliation. Per deploy/operator/internal/controller/AGENTS.md, the reconciler should continue with the object returned by the write, or treat informer lag as pending rather than a terminal failure. Consider threading the freshly created/adopted checkpoint object (or its verified binding) from the checkpoints reconciler into the Grove renderer instead of re-reading, or classifying a NotFound/stale result for a checkpoint created in this pass as pending and requeuing.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +125 to +153
func TestAutomaticCheckpointBinding(t *testing.T) {
owner := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd", Namespace: testNamespace, UID: types.UID("dgd-uid"),
}}
ckpt := mustExpectedAutoCheckpoint(
t,
testScheme(),
testIdentity(),
corev1.PodTemplateSpec{Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: consts.MainContainerName,
Image: "worker:expected",
}},
}},
consts.MainContainerName,
nvidiacomv1alpha1.CheckpointDeletionPolicyDelete,
nil,
owner,
)
ckpt.UID = types.UID("checkpoint-uid")
ckpt.Generation = 7
ckpt.Status.CheckpointID = testHash

binding, err := AutomaticCheckpointBinding(ckpt)
require.NoError(t, err)
assert.Regexp(t, `^v1/checkpoint-uid/7/[0-9a-f]{64}$`, binding)
recomputed, err := AutomaticCheckpointBinding(ckpt.DeepCopy())
require.NoError(t, err)
assert.Equal(t, binding, recomputed)

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.

🟡 New checkpoint binding test does not narrate its steps as required by the repository test style

The new binding test contains no t.Log step headings (TestAutomaticCheckpointBinding at deploy/operator/internal/checkpoint/checkpoint_test.go:125-243), violating the mandatory Go test style for this module.
Impact: Test output does not tell the scenario's story, which the repository requires for reviewability.

Rule reference

deploy/operator/AGENTS.md, "Go Test Style": "Use t.Log to tell the test's story, with one heading before each block that implements a test step." The test has distinct steps (build the expected automatic checkpoint, compute and re-compute the binding, run the mutation table) with no headings. Other new tests added in this PR (for example TestValidateGroveCheckpointBindings) do follow the rule.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +77 to +108
func TestDCDRendererRejectsMissingOrStaleAutomaticCheckpointBinding(t *testing.T) {
ckpt := &v1alpha1.DynamoCheckpoint{
ObjectMeta: metav1.ObjectMeta{
Name: "checkpoint-worker",
Namespace: "default",
UID: types.UID("checkpoint-uid"),
Generation: 3,
Labels: map[string]string{
snapshotprotocol.CheckpointIDLabel: "checkpoint-id",
},
Annotations: map[string]string{
commonconsts.CheckpointAutoAnnotation: commonconsts.KubeLabelValueTrue,
},
},
}
unmarked := ckpt.DeepCopy()
unmarked.Name = "unmarked-checkpoint"
delete(unmarked.Annotations, commonconsts.CheckpointAutoAnnotation)
staleGeneration := ckpt.DeepCopy()
staleGeneration.Generation--
staleGenerationBinding, err := checkpoint.AutomaticCheckpointBinding(staleGeneration)
require.NoError(t, err)
replaced := ckpt.DeepCopy()
replaced.UID = types.UID("old-uid")
replacedBinding, err := checkpoint.AutomaticCheckpointBinding(replaced)
require.NoError(t, err)
matchingBinding, err := checkpoint.AutomaticCheckpointBinding(ckpt)
require.NoError(t, err)
reader := fake.NewClientBuilder().
WithScheme(scheme.Scheme).
WithObjects(ckpt, unmarked).
Build()

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.

🟡 New renderer binding test does not narrate its steps as required by the repository test style

The new renderer rejection test contains no t.Log step headings (TestDCDRendererRejectsMissingOrStaleAutomaticCheckpointBinding at deploy/operator/internal/controller/dynamocomponentdeployment_controller_test.go:77-170), violating the mandatory Go test style for this module.
Impact: Test output does not tell the scenario's story, which the repository requires for reviewability.

Rule reference

deploy/operator/AGENTS.md, "Go Test Style": "Use t.Log to tell the test's story, with one heading before each block that implements a test step." This test builds several checkpoint fixtures, runs a rejection table, and then asserts the unmarked-checkpoint case, all without headings.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +164 to +178
currentMetadata, currentTemplate := checkpointBindingCopies(current)
if currentMetadata == "" || currentTemplate == "" {
return fmt.Errorf(
"automatic checkpoint binding copies are required on existing DynamoComponentDeployment %s/%s; recreate the workload",
current.Namespace,
current.Name,
)
}
if currentMetadata != currentTemplate {
return fmt.Errorf(
"automatic checkpoint binding copies disagree on DynamoComponentDeployment %s/%s",
current.Namespace,
current.Name,
)
}

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.

🔍 Existing DCDs and PodCliqueSets without binding copies become permanently unreconcilable after upgrade

validateCheckpointBinding fails closed whenever the desired object carries a binding but the live DynamoComponentDeployment does not have both durable copies (metadata annotation and pod-template annotation), returning ...copies are required on existing DynamoComponentDeployment ...; recreate the workload. validateGroveCheckpointBindings (deploy/operator/internal/controller/dgd_grove_workload_renderer.go:249-256) does the same for existing PodCliqueSet cliques. Every checkpoint-enabled workload created by an operator version prior to this PR lacks those copies, so the first reconcile after upgrade returns an error for that DGD and never converges until an operator manually deletes the DCD/PCS — which for Grove means deleting all running pods. The behavior is clearly deliberate (both tables assert it), but the upgrade path deserves an explicit migration note or a one-time adoption branch that stamps the copies when the live object's referenced checkpoint still verifies.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +104 to +117
// Resolve and verify the exact automatic checkpoint object.
ckpt := &nvidiacomv1alpha1.DynamoCheckpoint{}
if err := h.client.Get(ctx, types.NamespacedName{
Namespace: podNamespace,
Name: checkpointName,
}, ckpt); err != nil {
return admission.Denied("bound automatic checkpoint is unavailable")
}
if err := verifyBoundAutomaticCheckpoint(ckpt, binding); err != nil {
logger.Error(err, "already-shaped restore target rejected because its binding is invalid",
"namespace", podNamespace, "pod", pod.Name, "checkpoint", checkpointName)
return admission.Denied("automatic checkpoint binding is missing or stale")
}
return admission.Allowed("bound pod is already checkpoint-shaped")

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.

🔍 Bound restore pods are denied at admission when their checkpoint disappears

The already-shaped branch now denies pod creation whenever the pod carries a binding and the referenced DynamoCheckpoint cannot be read or no longer carries the automatic marker. Previously any already-shaped pod was admitted unchanged. Consequently, if the automatic checkpoint is deleted (or transiently missing from the webhook client's cache) while a bound workload is still running with a stamped pod template, every replacement pod for that workload is rejected, so the workload cannot recover replicas until the controller re-renders the pod template. Worth confirming that the DGD/DCD controllers always strip the binding annotations promptly when the checkpoint goes away, and that the webhook's client read is reliable (a cache miss here turns into a hard denial, not a retry).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +56 to +63
autoBinding := ""
automatic := ckpt.Annotations[consts.CheckpointAutoAnnotation] == consts.KubeLabelValueTrue
if automatic && ckpt.UID != "" && ckpt.Generation > 0 {
autoBinding, err = AutomaticCheckpointBinding(ckpt)
if err != nil {
return nil, err
}
}

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.

🔍 Binding computation now fails resolution for any automatic checkpoint whose ID label diverges

checkpointInfoFromObject now calls AutomaticCheckpointBinding for every checkpoint carrying the automatic marker, and automaticCheckpointBindingFor (deploy/operator/internal/checkpoint/resource.go:399-403) hard-errors when labels[CheckpointIDLabel] differs from the canonical CheckpointID(ckpt) (which prefers status.checkpointID, then status.identityHash, then the label). Any legacy automatic checkpoint whose status.identityHash was written from the identity hash rather than the DGD-scoped checkpoint ID will therefore make ResolveCheckpointForService return an error, failing the whole DGD/DCD reconcile rather than degrading to "no binding". The current controller keeps label and status in sync (internal/controller/dynamocheckpoint_controller.go:132-160), so this only affects objects written by older versions; still, an error here is unrecoverable without manual edits.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@galletas1712
galletas1712 force-pushed the fix/operator-checkpoint-workload-binding branch from a57a93a to d11873b Compare August 8, 2026 05:08
current *nvidiacomv1beta1.DynamoComponentDeployment,
desired *nvidiacomv1beta1.DynamoComponentDeployment,
) error {
const annotation = consts.CheckpointBindingAnnotation

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

validateCheckpointBinding declares annotation but never uses it, so the controller package will not compile. Fix: remove the unused local constant.

🤖 AI Fix

In deploy/operator/internal/controller/dgd_component_workloads_reconciler.go, inside validateCheckpointBinding, delete the line const annotation = consts.CheckpointBindingAnnotation.

ArtifactVersion string `json:"artifactVersion"`
DeletionPolicy string `json:"deletionPolicy"`
Controller *metav1.OwnerReference `json:"controller,omitempty"`
CaptureSpec nvidiacomv1alpha1.DynamoCheckpointSpec `json:"captureSpec"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Including deletion policy and controller owner references in the binding digest makes a lifecycle-only deletionPolicy change produce a new binding that the DCD/Grove validators reject as immutable. Fix: keep mutable lifecycle metadata out of the workload binding digest.

🤖 AI Fix

In deploy/operator/internal/checkpoint/resource.go, change the provenance used by AutomaticCheckpointBinding/VerifyAutomaticCheckpointBinding so it omits CheckpointDeletionPolicyAnnotation and metav1.GetControllerOf(ckpt), while leaving VerifyExpectedAutoCheckpoint's lifecycle validation unchanged.

@galletas1712
galletas1712 force-pushed the fix/operator-checkpoint-workload-binding branch from d11873b to 0263444 Compare August 8, 2026 09:52
@galletas1712
galletas1712 force-pushed the fix/operator-checkpoint-workload-binding branch from 0263444 to 4d703a2 Compare August 8, 2026 11:55
@galletas1712
galletas1712 force-pushed the fix/operator-checkpoint-workload-binding branch 2 times, most recently from 6acae2b to e8b2de8 Compare August 8, 2026 17:19
@galletas1712
galletas1712 force-pushed the fix/operator-checkpoint-workload-binding branch from e8b2de8 to 6dfe3fb Compare August 8, 2026 21:16
@galletas1712
galletas1712 requested a review from a team as a code owner August 9, 2026 01:27
@galletas1712
galletas1712 force-pushed the fix/operator-checkpoint-workload-binding branch from 6dfe3fb to f89cf09 Compare August 9, 2026 01:27
Signed-off-by: Schwinn Saereesitthipitak <schwinns@nvidia.com>
@galletas1712
galletas1712 force-pushed the fix/operator-checkpoint-workload-binding branch from f89cf09 to b941527 Compare August 9, 2026 05:08
@galletas1712

Copy link
Copy Markdown
Contributor Author

Final exact-source AKS validation (72295be)

  • Stack: #12903
  • Provenance: exact composite source 72295be01e003be86e1ff50074328077a45d2f05; tree becb4ac2fd5ca6892c70562f71706e2c45a36e8b.
  • Operator: OCI index sha256:754cec499be684112bfbdfa448d1d4df1d4b30408af88195e3802d1f8206edd5; amd64 sha256:1ff9fb35d2068e6170e2fb405ac79b95791a7577d1579cf6e6aca5c0257e168a.
  • Rollout safety: namespace-scoped rollout reached 1/1 Ready; CRDs untouched.
  • Ordinary TP2 restore: checkpoint checkpoint-5d2f7c0d202210efee4455fa50f3727d; immutable binding v1/89acc792-c13d-4302-9dab-bb4e8e143eac/1/942c43260c8204ad6867ebea038ea31cc37b390d406fbf1ee1f6d9cd700b1c73; 2 GPUs/ranks; both GMS loaders restored 24 allocations / 610,271,232 bytes each; restore total 22.32s; health/models/chat HTTP 200.
  • Preservation: existing TP1 qwen3-gms-v1-snap-fo remained Ready and health/models/chat returned HTTP 200 after rollout; prior full TP1 automatic failover already passed. Smoke resources were cleaned; checkpoint and TP1 were preserved.
  • Scope: TP2 automatic failover is still intentionally unsupported. This TP2 result proves ordinary capture/restore, identity, provenance, and binding; automatic Snapshot failover remains TP=PP=DP=1.
  • CI: targeted infrastructure reruns are pending; this does not claim full CI is green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deployment::k8s Relates to dynamo deployment in kubernetes fix size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant