1416 lines
44 KiB
Go
1416 lines
44 KiB
Go
/*
|
|
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
nvidiacomv1alpha1 "github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/v1alpha1"
|
|
commonController "github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/controller_common"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
batchv1 "k8s.io/api/batch/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/tools/record"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
const (
|
|
defaultNamespace = "default"
|
|
)
|
|
|
|
// MockRBACManager implements RBACManager for testing
|
|
type MockRBACManager struct {
|
|
EnsureServiceAccountWithRBACFunc func(ctx context.Context, targetNamespace, serviceAccountName, clusterRoleName string) error
|
|
}
|
|
|
|
func (m *MockRBACManager) EnsureServiceAccountWithRBAC(ctx context.Context, targetNamespace, serviceAccountName, clusterRoleName string) error {
|
|
if m.EnsureServiceAccountWithRBACFunc != nil {
|
|
return m.EnsureServiceAccountWithRBACFunc(ctx, targetNamespace, serviceAccountName, clusterRoleName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Helper function to create JSON config for tests
|
|
func createTestConfig(config map[string]interface{}) *apiextensionsv1.JSON {
|
|
jsonBytes, err := json.Marshal(config)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return &apiextensionsv1.JSON{Raw: jsonBytes}
|
|
}
|
|
|
|
var _ = Describe("DynamoGraphDeploymentRequest Controller", func() {
|
|
const (
|
|
timeout = time.Second * 10
|
|
interval = time.Millisecond * 250
|
|
)
|
|
|
|
var (
|
|
reconciler *DynamoGraphDeploymentRequestReconciler
|
|
recorder *record.FakeRecorder
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
recorder = record.NewFakeRecorder(100)
|
|
reconciler = &DynamoGraphDeploymentRequestReconciler{
|
|
Client: k8sClient,
|
|
Recorder: recorder,
|
|
Config: commonController.Config{
|
|
RestrictedNamespace: "",
|
|
RBAC: commonController.RBACConfig{
|
|
DGDRProfilingClusterRoleName: "test-cluster-role",
|
|
},
|
|
},
|
|
RBACManager: &MockRBACManager{},
|
|
}
|
|
})
|
|
|
|
Context("When reconciling initial DGDR", func() {
|
|
It("Should validate spec and transition to Pending", func() {
|
|
ctx := context.Background()
|
|
dgdrName := "test-dgdr-initial"
|
|
namespace := defaultNamespace
|
|
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "vllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"config": "/tmp/test-config.yaml",
|
|
},
|
|
"sla": map[string]interface{}{
|
|
"ttft": 100.0,
|
|
"itl": 1500.0,
|
|
"isl": 3000,
|
|
"osl": 5,
|
|
},
|
|
"hardware": map[string]interface{}{
|
|
"min_num_gpus_per_engine": 1,
|
|
"max_num_gpus_per_engine": 8,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(ctx, dgdr)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, dgdr) }()
|
|
|
|
// First reconcile: Empty -> Pending
|
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Check status
|
|
Eventually(func() string {
|
|
var updated nvidiacomv1alpha1.DynamoGraphDeploymentRequest
|
|
_ = k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, &updated)
|
|
return updated.Status.State
|
|
}, timeout, interval).Should(Equal(StatePending))
|
|
|
|
// Verify observedGeneration is set
|
|
var updated nvidiacomv1alpha1.DynamoGraphDeploymentRequest
|
|
_ = k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, &updated)
|
|
Expect(updated.Status.ObservedGeneration).Should(Equal(updated.Generation))
|
|
})
|
|
|
|
It("Should pass validation with minimal config", func() {
|
|
ctx := context.Background()
|
|
dgdrName := "test-dgdr-minimal"
|
|
namespace := defaultNamespace
|
|
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "vllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"sla": map[string]interface{}{
|
|
"ttft": 100.0,
|
|
"itl": 1500.0,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(ctx, dgdr)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, dgdr) }()
|
|
|
|
// Reconcile - should succeed with minimal config
|
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Check status transitions to Pending (not Failed)
|
|
Eventually(func() string {
|
|
var updated nvidiacomv1alpha1.DynamoGraphDeploymentRequest
|
|
_ = k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, &updated)
|
|
return updated.Status.State
|
|
}, timeout, interval).Should(Equal(StatePending))
|
|
})
|
|
})
|
|
|
|
Context("When creating profiling job", func() {
|
|
It("Should create online profiling job", func() {
|
|
ctx := context.Background()
|
|
dgdrName := "test-dgdr-profiling-online"
|
|
namespace := defaultNamespace
|
|
|
|
// Create ConfigMap for DGD base config
|
|
configMap := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-config",
|
|
Namespace: namespace,
|
|
},
|
|
Data: map[string]string{
|
|
"disagg.yaml": "test: config",
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, configMap)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, configMap) }()
|
|
|
|
// Create ServiceAccount
|
|
sa := &corev1.ServiceAccount{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: ServiceAccountProfilingJob,
|
|
Namespace: namespace,
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, sa)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, sa) }()
|
|
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "vllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"profiler_image": "test-profiler:latest",
|
|
},
|
|
"sla": map[string]interface{}{
|
|
"ttft": 100.0,
|
|
"itl": 1500.0,
|
|
"isl": 3000,
|
|
"osl": 5,
|
|
},
|
|
"hardware": map[string]interface{}{
|
|
"min_num_gpus_per_engine": 1,
|
|
"max_num_gpus_per_engine": 8,
|
|
},
|
|
}),
|
|
ConfigMapRef: &nvidiacomv1alpha1.ConfigMapKeySelector{
|
|
Name: "test-config",
|
|
Key: "disagg.yaml",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(ctx, dgdr)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, dgdr) }()
|
|
|
|
// Reconcile multiple times to move through states
|
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: dgdrName, Namespace: namespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Second reconcile: Pending -> Profiling
|
|
_, err = reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: dgdrName, Namespace: namespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Verify profiling job was created
|
|
Eventually(func() bool {
|
|
jobName := getProfilingJobName(dgdr)
|
|
job := &batchv1.Job{}
|
|
err := k8sClient.Get(ctx, types.NamespacedName{Name: jobName, Namespace: namespace}, job)
|
|
return err == nil
|
|
}, timeout, interval).Should(BeTrue())
|
|
|
|
// Verify job has correct labels
|
|
jobName := getProfilingJobName(dgdr)
|
|
job := &batchv1.Job{}
|
|
_ = k8sClient.Get(ctx, types.NamespacedName{Name: jobName, Namespace: namespace}, job)
|
|
Expect(job.Labels[LabelApp]).Should(Equal(LabelValueDynamoProfiler))
|
|
Expect(job.Labels[LabelDGDR]).Should(Equal(dgdrName))
|
|
|
|
// Verify job has profiler container
|
|
Expect(job.Spec.Template.Spec.Containers).Should(HaveLen(2))
|
|
Expect(job.Spec.Template.Spec.Containers[0].Name).Should(Equal(ContainerNameProfiler))
|
|
Expect(job.Spec.Template.Spec.Containers[1].Name).Should(Equal(ContainerNameOutputCopier))
|
|
|
|
// Verify emptyDir volume (not PVC)
|
|
Expect(job.Spec.Template.Spec.Volumes).Should(ContainElement(
|
|
corev1.Volume{
|
|
Name: VolumeNameProfilingOutput,
|
|
VolumeSource: corev1.VolumeSource{
|
|
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
|
},
|
|
},
|
|
))
|
|
|
|
// Clean up job
|
|
_ = k8sClient.Delete(ctx, job)
|
|
})
|
|
|
|
It("Should create offline (AIC) profiling job", func() {
|
|
ctx := context.Background()
|
|
dgdrName := "test-dgdr-profiling-aic"
|
|
namespace := defaultNamespace
|
|
|
|
// Create ServiceAccount
|
|
sa := &corev1.ServiceAccount{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: ServiceAccountProfilingJob,
|
|
Namespace: namespace,
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, sa)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, sa) }()
|
|
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "trtllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"config": "/tmp/test-config.yaml",
|
|
"profiler_image": "test-profiler:latest",
|
|
},
|
|
"sla": map[string]interface{}{
|
|
"ttft": 100.0,
|
|
"itl": 1500.0,
|
|
"isl": 3000,
|
|
"osl": 5,
|
|
},
|
|
"hardware": map[string]interface{}{
|
|
"min_num_gpus_per_engine": 1,
|
|
"max_num_gpus_per_engine": 8,
|
|
},
|
|
"sweep": map[string]interface{}{
|
|
"use_ai_configurator": true,
|
|
"aic_system": "h200_sxm",
|
|
"aic_model_name": "QWEN3_32B",
|
|
"aic_backend_version": "0.20.0",
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(ctx, dgdr)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, dgdr) }()
|
|
|
|
// Reconcile
|
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: dgdrName, Namespace: namespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
_, err = reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: dgdrName, Namespace: namespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Verify job was created with AIC label
|
|
Eventually(func() string {
|
|
jobName := getProfilingJobName(dgdr)
|
|
job := &batchv1.Job{}
|
|
if err := k8sClient.Get(ctx, types.NamespacedName{Name: jobName, Namespace: namespace}, job); err != nil {
|
|
return ""
|
|
}
|
|
return job.Labels[LabelApp]
|
|
}, timeout, interval).Should(Equal(LabelValueAICProfiler))
|
|
|
|
// Clean up
|
|
jobName := getProfilingJobName(dgdr)
|
|
job := &batchv1.Job{}
|
|
if err := k8sClient.Get(ctx, types.NamespacedName{Name: jobName, Namespace: namespace}, job); err == nil {
|
|
_ = k8sClient.Delete(ctx, job)
|
|
}
|
|
})
|
|
})
|
|
|
|
Context("When profiling completes", func() {
|
|
It("Should generate DGD spec from ConfigMap", func() {
|
|
ctx := context.Background()
|
|
dgdrName := "test-dgdr-profiling-complete"
|
|
namespace := defaultNamespace
|
|
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "vllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"config": "/tmp/test-config.yaml",
|
|
},
|
|
"sla": map[string]interface{}{
|
|
"ttft": 100.0,
|
|
"itl": 1500.0,
|
|
"isl": 3000,
|
|
"osl": 5,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(ctx, dgdr)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, dgdr) }()
|
|
|
|
// Update status to Profiling using Status subresource
|
|
dgdr.Status.State = StateProfiling
|
|
Expect(k8sClient.Status().Update(ctx, dgdr)).Should(Succeed())
|
|
|
|
// Create completed profiling job
|
|
jobName := getProfilingJobName(dgdr)
|
|
job := &batchv1.Job{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: jobName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: batchv1.JobSpec{
|
|
Template: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{{
|
|
Name: "test",
|
|
Image: "test",
|
|
}},
|
|
RestartPolicy: corev1.RestartPolicyNever,
|
|
},
|
|
},
|
|
},
|
|
Status: batchv1.JobStatus{
|
|
Conditions: []batchv1.JobCondition{{
|
|
Type: batchv1.JobComplete,
|
|
Status: corev1.ConditionTrue,
|
|
}},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, job)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, job) }()
|
|
|
|
// Update job status to completed using Status subresource
|
|
job.Status.Conditions = []batchv1.JobCondition{{
|
|
Type: batchv1.JobComplete,
|
|
Status: corev1.ConditionTrue,
|
|
}}
|
|
Expect(k8sClient.Status().Update(ctx, job)).Should(Succeed())
|
|
|
|
// Create output ConfigMap with DGD spec
|
|
dgdYAML := `apiVersion: nvidia.com/v1alpha1
|
|
kind: DynamoGraphDeployment
|
|
metadata:
|
|
name: test-dgd
|
|
spec:
|
|
services:
|
|
Frontend:
|
|
replicas: 1`
|
|
|
|
outputConfigMapName := getOutputConfigMapName(dgdr)
|
|
cm := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: outputConfigMapName,
|
|
Namespace: namespace,
|
|
},
|
|
Data: map[string]string{
|
|
ProfilingOutputFile: dgdYAML,
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, cm)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, cm) }()
|
|
|
|
// Reconcile to process the profiling completion
|
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: dgdrName, Namespace: namespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Get the updated DGDR
|
|
var updated nvidiacomv1alpha1.DynamoGraphDeploymentRequest
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, &updated)).Should(Succeed())
|
|
|
|
// Check that DGD spec was generated
|
|
Expect(updated.Status.GeneratedDeployment).NotTo(BeNil())
|
|
|
|
// Verify state transitioned to Ready (since autoApply is false by default)
|
|
Expect(updated.Status.State).Should(Equal(StateReady))
|
|
})
|
|
})
|
|
|
|
Context("When autoApply is enabled", func() {
|
|
It("Should create DGD after profiling", func() {
|
|
ctx := context.Background()
|
|
dgdrName := "test-dgdr-autoapply"
|
|
namespace := defaultNamespace
|
|
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "vllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"config": "/tmp/test-config.yaml",
|
|
},
|
|
"sla": map[string]interface{}{
|
|
"ttft": 100.0,
|
|
"itl": 1500.0,
|
|
"isl": 3000,
|
|
"osl": 5,
|
|
},
|
|
}),
|
|
},
|
|
AutoApply: true,
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(ctx, dgdr)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, dgdr) }()
|
|
|
|
// Update status to Profiling using Status subresource
|
|
dgdr.Status.State = StateProfiling
|
|
Expect(k8sClient.Status().Update(ctx, dgdr)).Should(Succeed())
|
|
|
|
// Create completed profiling job
|
|
jobName := getProfilingJobName(dgdr)
|
|
job := &batchv1.Job{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: jobName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: batchv1.JobSpec{
|
|
Template: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{{
|
|
Name: "test",
|
|
Image: "test",
|
|
}},
|
|
RestartPolicy: corev1.RestartPolicyNever,
|
|
},
|
|
},
|
|
},
|
|
Status: batchv1.JobStatus{
|
|
Conditions: []batchv1.JobCondition{{
|
|
Type: batchv1.JobComplete,
|
|
Status: corev1.ConditionTrue,
|
|
}},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, job)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, job) }()
|
|
|
|
// Update job status to completed using Status subresource
|
|
job.Status.Conditions = []batchv1.JobCondition{{
|
|
Type: batchv1.JobComplete,
|
|
Status: corev1.ConditionTrue,
|
|
}}
|
|
Expect(k8sClient.Status().Update(ctx, job)).Should(Succeed())
|
|
|
|
// Create output ConfigMap
|
|
dgdYAML := `apiVersion: nvidia.com/v1alpha1
|
|
kind: DynamoGraphDeployment
|
|
metadata:
|
|
name: test-dgd-auto
|
|
spec:
|
|
services:
|
|
Frontend:
|
|
replicas: 1`
|
|
|
|
outputConfigMapName := getOutputConfigMapName(dgdr)
|
|
cm := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: outputConfigMapName,
|
|
Namespace: namespace,
|
|
},
|
|
Data: map[string]string{
|
|
ProfilingOutputFile: dgdYAML,
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, cm)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, cm) }()
|
|
|
|
// Reconcile to generate spec (transitions to Deploying because autoApply=true)
|
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: dgdrName, Namespace: namespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Get updated DGDR and check state is Deploying
|
|
var updated nvidiacomv1alpha1.DynamoGraphDeploymentRequest
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, &updated)).Should(Succeed())
|
|
Expect(updated.Status.State).Should(Equal(StateDeploying))
|
|
|
|
// Reconcile again to create DGD
|
|
_, err = reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: dgdrName, Namespace: namespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Verify DGD was created
|
|
dgd := &nvidiacomv1alpha1.DynamoGraphDeployment{}
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-dgd-auto", Namespace: namespace}, dgd)).Should(Succeed())
|
|
|
|
// Get final DGDR status
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, &updated)).Should(Succeed())
|
|
Expect(updated.Status.Deployment).NotTo(BeNil())
|
|
Expect(updated.Status.Deployment.Created).Should(BeTrue())
|
|
Expect(updated.Status.Deployment.Name).Should(Equal("test-dgd-auto"))
|
|
|
|
// Clean up DGD
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-dgd-auto", Namespace: namespace}, dgd)).Should(Succeed())
|
|
_ = k8sClient.Delete(ctx, dgd)
|
|
})
|
|
})
|
|
|
|
Context("When enforcing spec immutability", func() {
|
|
It("Should reject spec changes after profiling starts", func() {
|
|
ctx := context.Background()
|
|
dgdrName := "test-dgdr-immutable"
|
|
namespace := defaultNamespace
|
|
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "vllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"config": "/tmp/test-config.yaml",
|
|
},
|
|
"sla": map[string]interface{}{
|
|
"ttft": 100.0,
|
|
"itl": 1500.0,
|
|
"isl": 3000,
|
|
"osl": 5,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(ctx, dgdr)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, dgdr) }()
|
|
|
|
// Reconcile to initialize
|
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: dgdrName, Namespace: namespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Get current generation
|
|
var current nvidiacomv1alpha1.DynamoGraphDeploymentRequest
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, ¤t)).Should(Succeed())
|
|
initialGeneration := current.Generation
|
|
observedGeneration := current.Status.ObservedGeneration
|
|
|
|
// Manually set state to Profiling to simulate in-progress profiling
|
|
current.Status.State = StateProfiling
|
|
Expect(k8sClient.Status().Update(ctx, ¤t)).Should(Succeed())
|
|
|
|
// Try to modify spec
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, ¤t)).Should(Succeed())
|
|
// Unmarshal config, modify it, and marshal back
|
|
var config map[string]interface{}
|
|
Expect(yaml.Unmarshal(current.Spec.ProfilingConfig.Config.Raw, &config)).Should(Succeed())
|
|
config["sla"].(map[string]interface{})["ttft"] = 200.0
|
|
current.Spec.ProfilingConfig.Config = createTestConfig(config)
|
|
Expect(k8sClient.Update(ctx, ¤t)).Should(Succeed())
|
|
|
|
// Reconcile
|
|
_, err = reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: dgdrName, Namespace: namespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Verify generation changed but observedGeneration stayed the same
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, ¤t)).Should(Succeed())
|
|
Expect(current.Generation).Should(BeNumerically(">", initialGeneration))
|
|
Expect(current.Status.ObservedGeneration).Should(Equal(observedGeneration))
|
|
Expect(current.Status.State).Should(Equal(StateProfiling)) // State unchanged
|
|
|
|
// Verify event was recorded
|
|
Eventually(func() bool {
|
|
select {
|
|
case event := <-recorder.Events:
|
|
return event == "Warning SpecChangeRejected Cannot modify spec in state 'Profiling'. DynamoGraphDeploymentRequest is immutable once profiling starts. Create a new resource with a different name instead."
|
|
default:
|
|
return false
|
|
}
|
|
}, timeout, interval).Should(BeTrue())
|
|
})
|
|
})
|
|
|
|
Context("When handling DGD deletion", func() {
|
|
It("Should transition to DeploymentDeleted state", func() {
|
|
ctx := context.Background()
|
|
dgdrName := "test-dgdr-dgd-deleted"
|
|
namespace := defaultNamespace
|
|
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "vllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"config": "/tmp/test-config.yaml",
|
|
},
|
|
"sla": map[string]interface{}{
|
|
"ttft": 100.0,
|
|
"itl": 1500.0,
|
|
"isl": 3000,
|
|
"osl": 5,
|
|
},
|
|
}),
|
|
},
|
|
AutoApply: true,
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(ctx, dgdr)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, dgdr) }()
|
|
|
|
// Update status to Ready with Deployment info using Status subresource
|
|
dgdr.Status.State = StateReady
|
|
dgdr.Status.Deployment = &nvidiacomv1alpha1.DeploymentStatus{
|
|
Name: "test-dgd-to-delete",
|
|
Namespace: namespace,
|
|
Created: true,
|
|
State: "Ready",
|
|
}
|
|
Expect(k8sClient.Status().Update(ctx, dgdr)).Should(Succeed())
|
|
|
|
// Reconcile when DGD doesn't exist (simulating deletion)
|
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: dgdrName, Namespace: namespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Get updated DGDR and check state transitioned to DeploymentDeleted
|
|
var updated nvidiacomv1alpha1.DynamoGraphDeploymentRequest
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, &updated)).Should(Succeed())
|
|
Expect(updated.Status.State).Should(Equal(StateDeploymentDeleted))
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("DGDR Helper Functions", func() {
|
|
Context("getProfilingJobName", func() {
|
|
It("Should return correct job name", func() {
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgdr",
|
|
},
|
|
}
|
|
Expect(getProfilingJobName(dgdr)).Should(Equal("profile-test-dgdr"))
|
|
})
|
|
})
|
|
|
|
Context("getOutputConfigMapName", func() {
|
|
It("Should return correct ConfigMap name", func() {
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgdr",
|
|
},
|
|
}
|
|
Expect(getOutputConfigMapName(dgdr)).Should(Equal("dgdr-output-test-dgdr"))
|
|
})
|
|
})
|
|
|
|
Context("isOnlineProfiling", func() {
|
|
It("Should return true for online profiling (use_ai_configurator=false)", func() {
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"sweep": map[string]interface{}{
|
|
"use_ai_configurator": false,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
Expect(isOnlineProfiling(dgdr)).Should(BeTrue())
|
|
})
|
|
|
|
It("Should return false for AI Configurator profiling (use_ai_configurator=true)", func() {
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"sweep": map[string]interface{}{
|
|
"use_ai_configurator": true,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
Expect(isOnlineProfiling(dgdr)).Should(BeFalse())
|
|
})
|
|
|
|
It("Should return true by default when sweep section is missing", func() {
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"backend": "vllm",
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
Expect(isOnlineProfiling(dgdr)).Should(BeTrue())
|
|
})
|
|
|
|
It("Should return true by default when use_ai_configurator is not specified", func() {
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"sweep": map[string]interface{}{
|
|
"prefill_interpolation_granularity": 16,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
Expect(isOnlineProfiling(dgdr)).Should(BeTrue())
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("DGDR Validation", func() {
|
|
var reconciler *DynamoGraphDeploymentRequestReconciler
|
|
|
|
BeforeEach(func() {
|
|
reconciler = &DynamoGraphDeploymentRequestReconciler{
|
|
Client: k8sClient,
|
|
}
|
|
})
|
|
|
|
Context("validateSpec", func() {
|
|
It("Should pass validation for valid spec", func() {
|
|
ctx := context.Background()
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "vllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"config": "/tmp/test-config.yaml",
|
|
},
|
|
"sla": map[string]interface{}{
|
|
"ttft": 100.0,
|
|
"itl": 1500.0,
|
|
"isl": 3000,
|
|
"osl": 5,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
err := reconciler.validateSpec(ctx, dgdr)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
|
|
It("Should pass validation with minimal config", func() {
|
|
ctx := context.Background()
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "vllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"sla": map[string]interface{}{
|
|
"ttft": 100.0,
|
|
"itl": 1500.0,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
// Validation should pass - profiler will auto-generate missing config
|
|
err := reconciler.validateSpec(ctx, dgdr)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("DGDR Profiler Arguments", func() {
|
|
var reconciler *DynamoGraphDeploymentRequestReconciler
|
|
|
|
BeforeEach(func() {
|
|
reconciler = &DynamoGraphDeploymentRequestReconciler{
|
|
Client: k8sClient,
|
|
Recorder: record.NewFakeRecorder(100),
|
|
Config: commonController.Config{
|
|
RestrictedNamespace: "",
|
|
},
|
|
RBACManager: &MockRBACManager{},
|
|
}
|
|
})
|
|
|
|
Context("When creating profiling job with inline config", func() {
|
|
It("Should pass config as --profile-config argument for online profiling", func() {
|
|
ctx := context.Background()
|
|
namespace := "default"
|
|
dgdrName := "test-args-online"
|
|
|
|
// Create ServiceAccount
|
|
sa := &corev1.ServiceAccount{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: ServiceAccountProfilingJob,
|
|
Namespace: namespace,
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, sa)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, sa) }()
|
|
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "trtllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"config": "/tmp/test-config.yaml",
|
|
"profiler_image": "test-profiler:latest",
|
|
},
|
|
"sla": map[string]interface{}{
|
|
"ttft": 50.0,
|
|
"itl": 10.0,
|
|
"isl": 3000,
|
|
"osl": 500,
|
|
},
|
|
"hardware": map[string]interface{}{
|
|
"gpu_type": "h200_sxm",
|
|
"min_num_gpus_per_engine": 2,
|
|
"max_num_gpus_per_engine": 4,
|
|
},
|
|
"sweep": map[string]interface{}{
|
|
"use_ai_configurator": false,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(ctx, dgdr)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, dgdr) }()
|
|
|
|
// Re-fetch DGDR to get proper metadata from API server
|
|
var fetchedDGDR nvidiacomv1alpha1.DynamoGraphDeploymentRequest
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, &fetchedDGDR)).Should(Succeed())
|
|
|
|
// Create profiling job with properly initialized DGDR
|
|
err := reconciler.createProfilingJob(ctx, &fetchedDGDR)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Verify job was created
|
|
jobName := getProfilingJobName(&fetchedDGDR)
|
|
job := &batchv1.Job{}
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: jobName, Namespace: namespace}, job)).Should(Succeed())
|
|
|
|
// Verify profiler container has --profile-config argument
|
|
profilerContainer := job.Spec.Template.Spec.Containers[0]
|
|
args := profilerContainer.Args
|
|
|
|
// Check that --profile-config argument is present
|
|
Expect(args).Should(ContainElement("--profile-config"))
|
|
|
|
// Clean up
|
|
_ = k8sClient.Delete(ctx, job)
|
|
})
|
|
|
|
It("Should pass config with AI Configurator settings for offline profiling", func() {
|
|
ctx := context.Background()
|
|
namespace := defaultNamespace
|
|
dgdrName := "test-args-offline"
|
|
|
|
// Create ServiceAccount
|
|
sa := &corev1.ServiceAccount{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: ServiceAccountProfilingJob,
|
|
Namespace: namespace,
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, sa)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, sa) }()
|
|
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "trtllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"config": "/tmp/test-config.yaml",
|
|
"profiler_image": "test-profiler:latest",
|
|
},
|
|
"sla": map[string]interface{}{
|
|
"ttft": 50.0,
|
|
"itl": 10.0,
|
|
"isl": 3000,
|
|
"osl": 500,
|
|
},
|
|
"hardware": map[string]interface{}{
|
|
"gpu_type": "h200_sxm",
|
|
"min_num_gpus_per_engine": 1,
|
|
"max_num_gpus_per_engine": 8,
|
|
},
|
|
"sweep": map[string]interface{}{
|
|
"use_ai_configurator": true,
|
|
"aic_system": "h200_sxm",
|
|
"aic_model_name": "QWEN3_32B",
|
|
"aic_backend_version": "0.20.0",
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(ctx, dgdr)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, dgdr) }()
|
|
|
|
// Re-fetch DGDR to get proper metadata from API server
|
|
var fetchedDGDR nvidiacomv1alpha1.DynamoGraphDeploymentRequest
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, &fetchedDGDR)).Should(Succeed())
|
|
|
|
// Create profiling job with properly initialized DGDR
|
|
err := reconciler.createProfilingJob(ctx, &fetchedDGDR)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Verify job was created
|
|
jobName := getProfilingJobName(&fetchedDGDR)
|
|
job := &batchv1.Job{}
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: jobName, Namespace: namespace}, job)).Should(Succeed())
|
|
|
|
// Verify profiler container has --profile-config argument
|
|
profilerContainer := job.Spec.Template.Spec.Containers[0]
|
|
args := profilerContainer.Args
|
|
|
|
// Check that --profile-config argument is present
|
|
Expect(args).Should(ContainElement("--profile-config"))
|
|
|
|
// Clean up
|
|
_ = k8sClient.Delete(ctx, job)
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("DGDR Error Handling", func() {
|
|
var reconciler *DynamoGraphDeploymentRequestReconciler
|
|
var recorder *record.FakeRecorder
|
|
|
|
BeforeEach(func() {
|
|
recorder = record.NewFakeRecorder(100)
|
|
reconciler = &DynamoGraphDeploymentRequestReconciler{
|
|
Client: k8sClient,
|
|
Recorder: recorder,
|
|
Config: commonController.Config{
|
|
RestrictedNamespace: "",
|
|
},
|
|
RBACManager: &MockRBACManager{},
|
|
}
|
|
})
|
|
|
|
Context("When profiling job fails", func() {
|
|
It("Should capture detailed error from pod termination state", func() {
|
|
ctx := context.Background()
|
|
namespace := defaultNamespace
|
|
dgdrName := "test-error-capture"
|
|
|
|
dgdr := &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: dgdrName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{
|
|
Model: "test-model",
|
|
Backend: "vllm",
|
|
ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{
|
|
ProfilerImage: "test-profiler:latest",
|
|
Config: createTestConfig(map[string]interface{}{
|
|
"engine": map[string]interface{}{
|
|
"config": "/tmp/test-config.yaml",
|
|
},
|
|
"sla": map[string]interface{}{
|
|
"ttft": 100.0,
|
|
"itl": 1500.0,
|
|
"isl": 3000,
|
|
"osl": 5,
|
|
},
|
|
"hardware": map[string]interface{}{
|
|
"min_num_gpus_per_engine": 1,
|
|
"max_num_gpus_per_engine": 8,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
Expect(k8sClient.Create(ctx, dgdr)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, dgdr) }()
|
|
|
|
// Set status to Profiling
|
|
dgdr.Status.State = StateProfiling
|
|
Expect(k8sClient.Status().Update(ctx, dgdr)).Should(Succeed())
|
|
|
|
// Create failed job
|
|
jobName := getProfilingJobName(dgdr)
|
|
job := &batchv1.Job{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: jobName,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: batchv1.JobSpec{
|
|
Template: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{{
|
|
Name: ContainerNameProfiler,
|
|
Image: "test",
|
|
}},
|
|
RestartPolicy: corev1.RestartPolicyNever,
|
|
},
|
|
},
|
|
},
|
|
Status: batchv1.JobStatus{
|
|
Conditions: []batchv1.JobCondition{{
|
|
Type: batchv1.JobFailed,
|
|
Status: corev1.ConditionTrue,
|
|
Message: "BackoffLimitExceeded",
|
|
}},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, job)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, job) }()
|
|
|
|
// Update job status
|
|
job.Status.Conditions = []batchv1.JobCondition{{
|
|
Type: batchv1.JobFailed,
|
|
Status: corev1.ConditionTrue,
|
|
Message: "BackoffLimitExceeded",
|
|
}}
|
|
Expect(k8sClient.Status().Update(ctx, job)).Should(Succeed())
|
|
|
|
// Create failed pod with termination details
|
|
pod := &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: jobName + "-pod",
|
|
Namespace: namespace,
|
|
Labels: map[string]string{
|
|
"job-name": jobName,
|
|
},
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{{
|
|
Name: ContainerNameProfiler,
|
|
Image: "test",
|
|
}},
|
|
RestartPolicy: corev1.RestartPolicyNever,
|
|
},
|
|
Status: corev1.PodStatus{
|
|
Phase: corev1.PodFailed,
|
|
ContainerStatuses: []corev1.ContainerStatus{{
|
|
Name: ContainerNameProfiler,
|
|
State: corev1.ContainerState{
|
|
Terminated: &corev1.ContainerStateTerminated{
|
|
ExitCode: 1,
|
|
Reason: "Error",
|
|
Message: "ValueError: Invalid model name for AI Configurator",
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
Expect(k8sClient.Create(ctx, pod)).Should(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, pod) }()
|
|
|
|
// Reconcile - should capture error details
|
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: dgdrName, Namespace: namespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Verify DGDR transitioned to Failed state
|
|
var updated nvidiacomv1alpha1.DynamoGraphDeploymentRequest
|
|
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dgdrName, Namespace: namespace}, &updated)).Should(Succeed())
|
|
Expect(updated.Status.State).Should(Equal(StateFailed))
|
|
|
|
// Verify error condition contains detailed error
|
|
condition := meta.FindStatusCondition(updated.Status.Conditions, ConditionTypeProfiling)
|
|
Expect(condition).NotTo(BeNil())
|
|
Expect(condition.Status).Should(Equal(metav1.ConditionFalse))
|
|
Expect(condition.Message).Should(ContainSubstring("profiling job failed"))
|
|
})
|
|
})
|
|
|
|
Context("When parsing multi-document YAML", func() {
|
|
It("Should extract DGD from ConfigMap + DGD YAML", func() {
|
|
// Multi-document YAML with ConfigMap first, then DGD
|
|
multiDocYAML := `---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: test-config
|
|
namespace: default
|
|
data:
|
|
some-data: "value"
|
|
---
|
|
apiVersion: nvidia.com/v1alpha1
|
|
kind: DynamoGraphDeployment
|
|
metadata:
|
|
name: test-dgd
|
|
namespace: default
|
|
spec:
|
|
backendFramework: vllm
|
|
services: {}`
|
|
|
|
dgd, err := reconciler.extractDGDFromYAML([]byte(multiDocYAML))
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(dgd).NotTo(BeNil())
|
|
Expect(dgd.Kind).Should(Equal("DynamoGraphDeployment"))
|
|
Expect(dgd.Name).Should(Equal("test-dgd"))
|
|
Expect(dgd.Spec.BackendFramework).Should(Equal("vllm"))
|
|
})
|
|
|
|
It("Should extract DGD from single-document YAML", func() {
|
|
// Single document YAML without separator
|
|
singleDocYAML := `apiVersion: nvidia.com/v1alpha1
|
|
kind: DynamoGraphDeployment
|
|
metadata:
|
|
name: test-dgd-single
|
|
namespace: default
|
|
spec:
|
|
backendFramework: vllm
|
|
services: {}`
|
|
|
|
dgd, err := reconciler.extractDGDFromYAML([]byte(singleDocYAML))
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(dgd).NotTo(BeNil())
|
|
Expect(dgd.Kind).Should(Equal("DynamoGraphDeployment"))
|
|
Expect(dgd.Name).Should(Equal("test-dgd-single"))
|
|
})
|
|
|
|
It("Should handle DGD + ConfigMap order (DGD first)", func() {
|
|
// Multi-document YAML with DGD first, then ConfigMap
|
|
multiDocYAML := `---
|
|
apiVersion: nvidia.com/v1alpha1
|
|
kind: DynamoGraphDeployment
|
|
metadata:
|
|
name: test-dgd-first
|
|
namespace: default
|
|
spec:
|
|
backendFramework: vllm
|
|
services: {}
|
|
---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: test-config
|
|
namespace: default
|
|
data:
|
|
some-data: "value"`
|
|
|
|
dgd, err := reconciler.extractDGDFromYAML([]byte(multiDocYAML))
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(dgd).NotTo(BeNil())
|
|
Expect(dgd.Kind).Should(Equal("DynamoGraphDeployment"))
|
|
Expect(dgd.Name).Should(Equal("test-dgd-first"))
|
|
})
|
|
|
|
It("Should return error when no DGD found", func() {
|
|
// YAML with only ConfigMap
|
|
configMapOnlyYAML := `---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: test-config
|
|
namespace: default
|
|
data:
|
|
some-data: "value"`
|
|
|
|
_, err := reconciler.extractDGDFromYAML([]byte(configMapOnlyYAML))
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.Error()).Should(ContainSubstring("no DynamoGraphDeployment found"))
|
|
})
|
|
|
|
It("Should handle YAML with leading separator", func() {
|
|
// YAML starting with --- separator
|
|
yamlWithLeadingSeparator := `---
|
|
apiVersion: nvidia.com/v1alpha1
|
|
kind: DynamoGraphDeployment
|
|
metadata:
|
|
name: test-dgd-leading
|
|
namespace: default
|
|
spec:
|
|
backendFramework: vllm
|
|
services: {}`
|
|
|
|
dgd, err := reconciler.extractDGDFromYAML([]byte(yamlWithLeadingSeparator))
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(dgd).NotTo(BeNil())
|
|
Expect(dgd.Name).Should(Equal("test-dgd-leading"))
|
|
})
|
|
|
|
It("Should extract DGD and additional resources correctly", func() {
|
|
// Multi-document YAML with ConfigMap and DGD
|
|
multiDocYAML := `---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: model-config
|
|
namespace: default
|
|
data:
|
|
model.json: '{"name": "test-model"}'
|
|
---
|
|
apiVersion: nvidia.com/v1alpha1
|
|
kind: DynamoGraphDeployment
|
|
metadata:
|
|
name: test-dgd
|
|
namespace: default
|
|
spec:
|
|
backendFramework: vllm
|
|
services: {}`
|
|
|
|
dgd, additionalResources, err := reconciler.extractResourcesFromYAML([]byte(multiDocYAML))
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(dgd).NotTo(BeNil())
|
|
Expect(dgd.Name).Should(Equal("test-dgd"))
|
|
Expect(additionalResources).To(HaveLen(1))
|
|
Expect(additionalResources[0].GetKind()).Should(Equal("ConfigMap"))
|
|
Expect(additionalResources[0].GetName()).Should(Equal("model-config"))
|
|
})
|
|
|
|
It("Should handle multiple additional resources", func() {
|
|
// Multi-document YAML with multiple ConfigMaps and DGD
|
|
multiDocYAML := `---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: config1
|
|
data:
|
|
key1: value1
|
|
---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: config2
|
|
data:
|
|
key2: value2
|
|
---
|
|
apiVersion: nvidia.com/v1alpha1
|
|
kind: DynamoGraphDeployment
|
|
metadata:
|
|
name: test-dgd
|
|
spec:
|
|
backendFramework: vllm
|
|
services: {}`
|
|
|
|
dgd, additionalResources, err := reconciler.extractResourcesFromYAML([]byte(multiDocYAML))
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(dgd).NotTo(BeNil())
|
|
Expect(additionalResources).To(HaveLen(2))
|
|
Expect(additionalResources[0].GetName()).Should(Equal("config1"))
|
|
Expect(additionalResources[1].GetName()).Should(Equal("config2"))
|
|
})
|
|
})
|
|
})
|