@@ -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