Skip to content

Commit 76b1bcd

Browse files
test/e2e: extend e2e tests and fix flaky MIG placement test
1 parent ece08ee commit 76b1bcd

3 files changed

Lines changed: 193 additions & 7 deletions

File tree

deploy/00_instaslice-operator.crd.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.17.2
6+
controller-gen.kubebuilder.io/version: v0.19.0
77
name: dasoperators.inference.redhat.com
88
spec:
99
group: inference.redhat.com

deploy/00_nodeaccelerators.crd.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.17.2
6+
controller-gen.kubebuilder.io/version: v0.19.0
77
name: nodeaccelerators.inference.redhat.com
88
spec:
99
group: inference.redhat.com

test/e2e/e2e.go

Lines changed: 191 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -741,25 +741,45 @@ var _ = Describe("MIG placement start index", Ordered, func() {
741741
}
742742
})
743743

744-
It("should set start index to 0 in AllocationClaims", func(ctx SpecContext) {
744+
It("should have valid MIG placements in AllocationClaims", func(ctx SpecContext) {
745+
// This test verifies that each pod gets a valid MIG placement.
746+
// When pods are scheduled on different GPUs (parallel scheduling with staged claims),
747+
// both should have start=0. When pods are scheduled on the same GPU (sequential
748+
// scheduling), they should have unique start indices (e.g., 0 and 1).
745749
Eventually(func() (bool, error) {
746750
allocs, err := dasClient.OpenShiftOperatorV1alpha1().AllocationClaims("das-operator").List(ctx, metav1.ListOptions{})
747751
if err != nil {
748752
return false, err
749753
}
754+
755+
gpuPlacements := make(map[string][]int32)
750756
found := 0
751757
for _, alloc := range allocs.Items {
752758
var spec instav1.AllocationClaimSpec
753759
if err := json.Unmarshal(alloc.Spec.Raw, &spec); err != nil {
754760
continue
755761
}
756762

757-
if spec.MigPlacement.Start != 0 { // both pods should have start index 0
758-
return false, nil
759-
}
763+
gpuPlacements[spec.GPUUUID] = append(gpuPlacements[spec.GPUUUID], spec.MigPlacement.Start)
760764
found++
761765
}
762-
return found == len(podNames), nil
766+
767+
if found != len(podNames) {
768+
return false, nil
769+
}
770+
771+
// Validate: start indices must be unique per GPU.
772+
for _, starts := range gpuPlacements {
773+
seen := make(map[int32]bool)
774+
for _, start := range starts {
775+
if seen[start] {
776+
return false, nil
777+
}
778+
seen[start] = true
779+
}
780+
}
781+
782+
return true, nil
763783
}, 5*time.Minute, 5*time.Second).Should(BeTrue())
764784
})
765785
})
@@ -1190,3 +1210,169 @@ func findCondition(conditions []operatorv1.OperatorCondition, conditionType stri
11901210
}
11911211
return nil
11921212
}
1213+
1214+
var _ = Describe("GPU memory resource capacity", Ordered, func() {
1215+
BeforeAll(func() {
1216+
if os.Getenv("KUBECONFIG") == "" {
1217+
Skip("KUBECONFIG is not set; skipping e2e test")
1218+
}
1219+
})
1220+
1221+
It("should advertise gpu.das.openshift.io/mem capacity on nodes with GPUs", func(ctx SpecContext) {
1222+
Eventually(func() (int, error) {
1223+
nodes, err := kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
1224+
if err != nil {
1225+
return 0, err
1226+
}
1227+
1228+
gpuMemResourceName := corev1.ResourceName("gpu.das.openshift.io/mem")
1229+
nodesWithGPUMem := 0
1230+
1231+
for _, node := range nodes.Items {
1232+
if q, ok := node.Status.Capacity[gpuMemResourceName]; ok {
1233+
By(fmt.Sprintf("node %s has gpu.das.openshift.io/mem capacity: %s", node.Name, q.String()))
1234+
if q.Value() > 0 {
1235+
nodesWithGPUMem++
1236+
}
1237+
}
1238+
}
1239+
return nodesWithGPUMem, nil
1240+
}, 2*time.Minute, 5*time.Second).Should(BeNumerically(">", 0), "at least one node should advertise gpu.das.openshift.io/mem")
1241+
})
1242+
1243+
It("should have NodeAccelerator resources with GPU information", func(ctx SpecContext) {
1244+
Eventually(func() (int, error) {
1245+
nodeAccels, err := dasClient.OpenShiftOperatorV1alpha1().NodeAccelerators(dasOperatorNamespace).List(ctx, metav1.ListOptions{})
1246+
if err != nil {
1247+
return 0, err
1248+
}
1249+
1250+
validAccelerators := 0
1251+
for _, accel := range nodeAccels.Items {
1252+
if len(accel.Status.NodeResources.Raw) == 0 {
1253+
continue
1254+
}
1255+
1256+
var discovered instav1.DiscoveredNodeResources
1257+
if err := json.Unmarshal(accel.Status.NodeResources.Raw, &discovered); err != nil {
1258+
continue
1259+
}
1260+
1261+
if len(discovered.NodeGPUs) > 0 && len(discovered.MigPlacement) > 0 {
1262+
By(fmt.Sprintf("NodeAccelerator %s has %d GPUs and %d MIG profiles",
1263+
accel.Name, len(discovered.NodeGPUs), len(discovered.MigPlacement)))
1264+
validAccelerators++
1265+
}
1266+
}
1267+
return validAccelerators, nil
1268+
}, 2*time.Minute, 5*time.Second).Should(BeNumerically(">", 0), "at least one NodeAccelerator should have GPUs")
1269+
})
1270+
})
1271+
1272+
var _ = Describe("MIG profiles annotation scheduling", Ordered, func() {
1273+
var (
1274+
namespace string
1275+
podName string
1276+
)
1277+
1278+
BeforeAll(func() {
1279+
if os.Getenv("KUBECONFIG") == "" {
1280+
Skip("KUBECONFIG is not set; skipping e2e test")
1281+
}
1282+
})
1283+
1284+
BeforeAll(func() {
1285+
namespace = "das-e2e-mig-annotation"
1286+
podName = "mig-annotation-pod"
1287+
1288+
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}
1289+
By("creating namespace " + namespace)
1290+
_, err := kubeClient.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
1291+
if err != nil && !apierrors.IsAlreadyExists(err) {
1292+
Expect(err).NotTo(HaveOccurred())
1293+
}
1294+
1295+
// Create a pod that uses the das.openshift.io/mig-profiles annotation
1296+
// instead of nvidia.com/mig-* resource requests (simulating Kueue-transformed pod)
1297+
pod := &corev1.Pod{
1298+
ObjectMeta: metav1.ObjectMeta{
1299+
Name: podName,
1300+
Namespace: namespace,
1301+
Annotations: map[string]string{
1302+
"das.openshift.io/mig-profiles": "1g.5gb:1",
1303+
},
1304+
},
1305+
Spec: corev1.PodSpec{
1306+
SchedulerName: "das-scheduler",
1307+
Containers: []corev1.Container{
1308+
{
1309+
Name: "test",
1310+
Image: "quay.io/prometheus/busybox",
1311+
Command: []string{"sh", "-c", "env && sleep 3600"},
1312+
Resources: corev1.ResourceRequirements{
1313+
Limits: corev1.ResourceList{
1314+
corev1.ResourceName("gpu.das.openshift.io/mem"): resource.MustParse("5"),
1315+
},
1316+
},
1317+
SecurityContext: &corev1.SecurityContext{
1318+
AllowPrivilegeEscalation: pointer.Bool(false),
1319+
Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}},
1320+
SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault},
1321+
},
1322+
},
1323+
},
1324+
RestartPolicy: corev1.RestartPolicyOnFailure,
1325+
},
1326+
}
1327+
1328+
Expect(waitForServiceReady(context.Background(), dasOperatorNamespace, webhookServiceName, 2*time.Minute)).To(Succeed())
1329+
By("creating test pod with mig-profiles annotation")
1330+
Expect(createPods(context.Background(), namespace, []*corev1.Pod{pod})).To(Succeed())
1331+
})
1332+
1333+
AfterAll(func() {
1334+
By("deleting namespace " + namespace)
1335+
err := kubeClient.CoreV1().Namespaces().Delete(context.Background(), namespace, metav1.DeleteOptions{})
1336+
Expect(err).NotTo(HaveOccurred())
1337+
1338+
Eventually(func() bool {
1339+
_, err := kubeClient.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{})
1340+
return apierrors.IsNotFound(err)
1341+
}, 5*time.Minute, time.Second).Should(BeTrue())
1342+
})
1343+
1344+
It("should schedule pod using mig-profiles annotation", func(ctx SpecContext) {
1345+
Eventually(func() (corev1.PodPhase, error) {
1346+
p, err := kubeClient.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
1347+
if err != nil {
1348+
return "", err
1349+
}
1350+
return p.Status.Phase, nil
1351+
}, 60*time.Minute, 5*time.Second).Should(Equal(corev1.PodRunning))
1352+
})
1353+
1354+
It("should create AllocationClaim for pod with mig-profiles annotation", func(ctx SpecContext) {
1355+
Eventually(func() (bool, error) {
1356+
allocs, err := dasClient.OpenShiftOperatorV1alpha1().AllocationClaims(dasOperatorNamespace).List(ctx, metav1.ListOptions{})
1357+
if err != nil {
1358+
return false, err
1359+
}
1360+
1361+
for _, alloc := range allocs.Items {
1362+
var spec instav1.AllocationClaimSpec
1363+
if err := json.Unmarshal(alloc.Spec.Raw, &spec); err != nil {
1364+
continue
1365+
}
1366+
1367+
if spec.PodRef.Name == podName && spec.PodRef.Namespace == namespace {
1368+
By(fmt.Sprintf("found AllocationClaim %s with profile %s", alloc.Name, spec.Profile))
1369+
// Verify the profile matches what was in the annotation
1370+
if spec.Profile == "1g.5gb" {
1371+
return true, nil
1372+
}
1373+
}
1374+
}
1375+
return false, nil
1376+
}, 5*time.Minute, 5*time.Second).Should(BeTrue())
1377+
})
1378+
})

0 commit comments

Comments
 (0)