sigs.k8s.io/kueue@v0.6.2/pkg/scheduler/flavorassigner/flavorassigner_test.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package flavorassigner 18 19 import ( 20 "fmt" 21 "testing" 22 23 "github.com/go-logr/logr/testr" 24 "github.com/google/go-cmp/cmp" 25 "github.com/google/go-cmp/cmp/cmpopts" 26 corev1 "k8s.io/api/core/v1" 27 "k8s.io/apimachinery/pkg/api/resource" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/util/sets" 30 "k8s.io/utils/ptr" 31 32 kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1" 33 "sigs.k8s.io/kueue/pkg/cache" 34 "sigs.k8s.io/kueue/pkg/features" 35 utiltesting "sigs.k8s.io/kueue/pkg/util/testing" 36 "sigs.k8s.io/kueue/pkg/workload" 37 ) 38 39 func TestAssignFlavors(t *testing.T) { 40 defaultFlavorFungibility := kueue.FlavorFungibility{ 41 WhenCanBorrow: kueue.Borrow, 42 WhenCanPreempt: kueue.TryNextFlavor, 43 } 44 resourceFlavors := map[kueue.ResourceFlavorReference]*kueue.ResourceFlavor{ 45 "default": { 46 ObjectMeta: metav1.ObjectMeta{Name: "default"}, 47 }, 48 "one": utiltesting.MakeResourceFlavor("one").Label("type", "one").Obj(), 49 "two": utiltesting.MakeResourceFlavor("two").Label("type", "two").Obj(), 50 "b_one": utiltesting.MakeResourceFlavor("b_one").Label("b_type", "one").Obj(), 51 "b_two": utiltesting.MakeResourceFlavor("b_two").Label("b_type", "two").Obj(), 52 "tainted": utiltesting.MakeResourceFlavor("tainted"). 53 Taint(corev1.Taint{ 54 Key: "instance", 55 Value: "spot", 56 Effect: corev1.TaintEffectNoSchedule, 57 }).Obj(), 58 } 59 60 cases := map[string]struct { 61 wlPods []kueue.PodSet 62 wlReclaimablePods []kueue.ReclaimablePod 63 clusterQueue cache.ClusterQueue 64 wantRepMode FlavorAssignmentMode 65 wantAssignment Assignment 66 enableLendingLimit bool 67 }{ 68 "single flavor, fits": { 69 wlPods: []kueue.PodSet{ 70 *utiltesting.MakePodSet("main", 1). 71 Request(corev1.ResourceCPU, "1"). 72 Request(corev1.ResourceMemory, "1Mi"). 73 Obj(), 74 }, 75 clusterQueue: cache.ClusterQueue{ 76 ResourceGroups: []cache.ResourceGroup{{ 77 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourceMemory), 78 Flavors: []cache.FlavorQuotas{{ 79 Name: "default", 80 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 81 corev1.ResourceCPU: {Nominal: 1000}, 82 corev1.ResourceMemory: {Nominal: 2 * utiltesting.Mi}, 83 }, 84 }}, 85 }}, 86 }, 87 wantRepMode: Fit, 88 wantAssignment: Assignment{ 89 PodSets: []PodSetAssignment{ 90 { 91 Name: "main", 92 Flavors: ResourceAssignment{ 93 corev1.ResourceCPU: {Name: "default", Mode: Fit}, 94 corev1.ResourceMemory: {Name: "default", Mode: Fit}, 95 }, 96 Requests: corev1.ResourceList{ 97 corev1.ResourceCPU: resource.MustParse("1000m"), 98 corev1.ResourceMemory: resource.MustParse("1Mi"), 99 }, 100 Count: 1, 101 }, 102 }, 103 Usage: cache.FlavorResourceQuantities{ 104 "default": map[corev1.ResourceName]int64{ 105 corev1.ResourceCPU: 1000, 106 corev1.ResourceMemory: 1 * 1024 * 1024, 107 }, 108 }, 109 }, 110 }, 111 "single flavor, fits tainted flavor": { 112 wlPods: []kueue.PodSet{ 113 *utiltesting.MakePodSet("main", 1). 114 Request(corev1.ResourceCPU, "1"). 115 Toleration(corev1.Toleration{ 116 Key: "instance", 117 Operator: corev1.TolerationOpEqual, 118 Value: "spot", 119 Effect: corev1.TaintEffectNoSchedule, 120 }). 121 Obj(), 122 }, 123 clusterQueue: cache.ClusterQueue{ 124 ResourceGroups: []cache.ResourceGroup{{ 125 CoveredResources: sets.New(corev1.ResourceCPU), 126 Flavors: []cache.FlavorQuotas{{ 127 Name: "tainted", 128 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 129 corev1.ResourceCPU: {Nominal: 4000}, 130 }, 131 }}, 132 }}, 133 }, 134 wantRepMode: Fit, 135 wantAssignment: Assignment{ 136 PodSets: []PodSetAssignment{{ 137 Name: "main", 138 Flavors: ResourceAssignment{ 139 corev1.ResourceCPU: {Name: "tainted", Mode: Fit}, 140 }, 141 Requests: corev1.ResourceList{ 142 corev1.ResourceCPU: resource.MustParse("1000m"), 143 }, 144 Count: 1, 145 }}, 146 Usage: cache.FlavorResourceQuantities{ 147 "tainted": { 148 corev1.ResourceCPU: 1000, 149 }, 150 }, 151 }, 152 }, 153 "single flavor, used resources, doesn't fit": { 154 wlPods: []kueue.PodSet{ 155 *utiltesting.MakePodSet("main", 1). 156 Request(corev1.ResourceCPU, "2"). 157 Obj(), 158 }, 159 clusterQueue: cache.ClusterQueue{ 160 ResourceGroups: []cache.ResourceGroup{{ 161 CoveredResources: sets.New(corev1.ResourceCPU), 162 Flavors: []cache.FlavorQuotas{{ 163 Name: "default", 164 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 165 corev1.ResourceCPU: {Nominal: 4000}, 166 }, 167 }}, 168 }}, 169 Usage: cache.FlavorResourceQuantities{ 170 "default": {corev1.ResourceCPU: 3_000}, 171 }, 172 }, 173 wantRepMode: Preempt, 174 wantAssignment: Assignment{ 175 PodSets: []PodSetAssignment{{ 176 Name: "main", 177 Flavors: ResourceAssignment{ 178 corev1.ResourceCPU: {Name: "default", Mode: Preempt}, 179 }, 180 Requests: corev1.ResourceList{ 181 corev1.ResourceCPU: resource.MustParse("2000m"), 182 }, 183 Status: &Status{ 184 reasons: []string{"insufficient unused quota for cpu in flavor default, 1 more needed"}, 185 }, 186 Count: 1, 187 }}, 188 Usage: cache.FlavorResourceQuantities{ 189 "default": { 190 corev1.ResourceCPU: 2000, 191 }, 192 }, 193 }, 194 }, 195 "multiple resource groups, fits": { 196 wlPods: []kueue.PodSet{ 197 *utiltesting.MakePodSet("main", 1). 198 Request(corev1.ResourceCPU, "3"). 199 Request(corev1.ResourceMemory, "10Mi"). 200 Obj(), 201 }, 202 clusterQueue: cache.ClusterQueue{ 203 ResourceGroups: []cache.ResourceGroup{ 204 { 205 CoveredResources: sets.New(corev1.ResourceCPU), 206 Flavors: []cache.FlavorQuotas{ 207 { 208 Name: "one", 209 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 210 corev1.ResourceCPU: {Nominal: 2000}, 211 }, 212 }, 213 { 214 Name: "two", 215 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 216 corev1.ResourceCPU: {Nominal: 4000}, 217 }, 218 }, 219 }, 220 }, 221 { 222 CoveredResources: sets.New(corev1.ResourceMemory), 223 Flavors: []cache.FlavorQuotas{ 224 { 225 Name: "b_one", 226 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 227 corev1.ResourceMemory: {Nominal: utiltesting.Gi}, 228 }, 229 }, 230 { 231 Name: "b_two", 232 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 233 corev1.ResourceMemory: {Nominal: 5 * utiltesting.Gi}, 234 }, 235 }, 236 }, 237 }, 238 }, 239 }, 240 wantRepMode: Fit, 241 wantAssignment: Assignment{ 242 PodSets: []PodSetAssignment{{ 243 Name: "main", 244 Flavors: ResourceAssignment{ 245 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 246 corev1.ResourceMemory: {Name: "b_one", Mode: Fit}, 247 }, 248 Requests: corev1.ResourceList{ 249 corev1.ResourceCPU: resource.MustParse("3000m"), 250 corev1.ResourceMemory: resource.MustParse("10Mi"), 251 }, 252 Count: 1, 253 }}, 254 Usage: cache.FlavorResourceQuantities{ 255 "two": map[corev1.ResourceName]int64{ 256 corev1.ResourceCPU: 3000, 257 }, 258 "b_one": map[corev1.ResourceName]int64{ 259 corev1.ResourceMemory: 10 * 1024 * 1024, 260 }, 261 }, 262 }, 263 }, 264 "multiple resource groups, one could fit with preemption, other doesn't fit": { 265 wlPods: []kueue.PodSet{ 266 *utiltesting.MakePodSet("main", 1). 267 Request(corev1.ResourceCPU, "3"). 268 Request(corev1.ResourceMemory, "10Mi"). 269 Obj(), 270 }, 271 clusterQueue: cache.ClusterQueue{ 272 ResourceGroups: []cache.ResourceGroup{ 273 { 274 CoveredResources: sets.New(corev1.ResourceCPU), 275 Flavors: []cache.FlavorQuotas{{ 276 Name: "one", 277 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 278 corev1.ResourceCPU: {Nominal: 3000}, 279 }, 280 }}, 281 }, 282 { 283 CoveredResources: sets.New(corev1.ResourceMemory), 284 Flavors: []cache.FlavorQuotas{{ 285 Name: "b_one", 286 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 287 corev1.ResourceMemory: {Nominal: utiltesting.Mi}, 288 }, 289 }}, 290 }, 291 }, 292 Usage: cache.FlavorResourceQuantities{ 293 "one": {corev1.ResourceCPU: 1000}, 294 }, 295 }, 296 wantAssignment: Assignment{ 297 PodSets: []PodSetAssignment{{ 298 Name: "main", 299 Requests: corev1.ResourceList{ 300 corev1.ResourceCPU: resource.MustParse("3000m"), 301 corev1.ResourceMemory: resource.MustParse("10Mi"), 302 }, 303 Status: &Status{ 304 reasons: []string{ 305 "insufficient quota for memory in flavor b_one in ClusterQueue", 306 }, 307 }, 308 Count: 1, 309 }}, 310 Usage: cache.FlavorResourceQuantities{}, 311 }, 312 }, 313 "multiple resource groups with multiple resources, fits": { 314 wlPods: []kueue.PodSet{ 315 *utiltesting.MakePodSet("main", 1). 316 Request(corev1.ResourceCPU, "3"). 317 Request(corev1.ResourceMemory, "10Mi"). 318 Request("example.com/gpu", "3"). 319 Obj(), 320 }, 321 clusterQueue: cache.ClusterQueue{ 322 ResourceGroups: []cache.ResourceGroup{ 323 { 324 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourceMemory), 325 Flavors: []cache.FlavorQuotas{ 326 { 327 Name: "one", 328 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 329 corev1.ResourceCPU: {Nominal: 2000}, 330 corev1.ResourceMemory: {Nominal: utiltesting.Gi}, 331 }, 332 }, 333 { 334 Name: "two", 335 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 336 corev1.ResourceCPU: {Nominal: 4000}, 337 corev1.ResourceMemory: {Nominal: 15 * utiltesting.Mi}, 338 }, 339 }, 340 }, 341 }, 342 { 343 CoveredResources: sets.New[corev1.ResourceName]("example.com/gpu"), 344 Flavors: []cache.FlavorQuotas{ 345 { 346 Name: "b_one", 347 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 348 "example.com/gpu": {Nominal: 4}, 349 }, 350 }, 351 { 352 Name: "b_two", 353 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 354 "example.com/gpu": {Nominal: 2}, 355 }, 356 }, 357 }, 358 }, 359 }, 360 }, 361 wantRepMode: Fit, 362 wantAssignment: Assignment{ 363 PodSets: []PodSetAssignment{{ 364 Name: "main", 365 Flavors: ResourceAssignment{ 366 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 367 corev1.ResourceMemory: {Name: "two", Mode: Fit}, 368 "example.com/gpu": {Name: "b_one", Mode: Fit}, 369 }, 370 Requests: corev1.ResourceList{ 371 corev1.ResourceCPU: resource.MustParse("3000m"), 372 corev1.ResourceMemory: resource.MustParse("10Mi"), 373 "example.com/gpu": resource.MustParse("3"), 374 }, 375 Count: 1, 376 }}, 377 Usage: cache.FlavorResourceQuantities{ 378 "two": map[corev1.ResourceName]int64{ 379 corev1.ResourceCPU: 3000, 380 corev1.ResourceMemory: 10 * 1024 * 1024, 381 }, 382 "b_one": map[corev1.ResourceName]int64{ 383 "example.com/gpu": 3, 384 }, 385 }, 386 }, 387 }, 388 "multiple resource groups with multiple resources, fits with different modes": { 389 wlPods: []kueue.PodSet{ 390 *utiltesting.MakePodSet("main", 1). 391 Request(corev1.ResourceCPU, "3"). 392 Request(corev1.ResourceMemory, "10Mi"). 393 Request("example.com/gpu", "3"). 394 Obj(), 395 }, 396 clusterQueue: cache.ClusterQueue{ 397 ResourceGroups: []cache.ResourceGroup{ 398 { 399 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourceMemory), 400 Flavors: []cache.FlavorQuotas{ 401 { 402 Name: "one", 403 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 404 corev1.ResourceCPU: {Nominal: 2000}, 405 corev1.ResourceMemory: {Nominal: utiltesting.Gi}, 406 }, 407 }, 408 { 409 Name: "two", 410 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 411 corev1.ResourceCPU: {Nominal: 4000}, 412 corev1.ResourceMemory: {Nominal: 15 * utiltesting.Mi}, 413 }, 414 }, 415 }, 416 }, 417 { 418 CoveredResources: sets.New[corev1.ResourceName]("example.com/gpu"), 419 Flavors: []cache.FlavorQuotas{{ 420 Name: "b_one", 421 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 422 "example.com/gpu": {Nominal: 4}, 423 }, 424 }}, 425 }, 426 }, 427 Usage: cache.FlavorResourceQuantities{ 428 "two": {corev1.ResourceMemory: 10 * utiltesting.Mi}, 429 }, 430 Cohort: &cache.Cohort{ 431 RequestableResources: cache.FlavorResourceQuantities{ 432 "one": { 433 corev1.ResourceCPU: 2000, 434 corev1.ResourceMemory: utiltesting.Gi, 435 }, 436 "two": { 437 corev1.ResourceCPU: 4000, 438 corev1.ResourceMemory: 15 * utiltesting.Mi, 439 }, 440 "b_one": { 441 "example.com/gpu": 4, 442 }, 443 }, 444 Usage: cache.FlavorResourceQuantities{ 445 "two": { 446 corev1.ResourceMemory: 10 * utiltesting.Mi, 447 }, 448 "b_one": { 449 "example.com/gpu": 2, 450 }, 451 }, 452 }, 453 }, 454 wantRepMode: Preempt, 455 wantAssignment: Assignment{ 456 PodSets: []PodSetAssignment{{ 457 Name: "main", 458 Flavors: ResourceAssignment{ 459 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 460 corev1.ResourceMemory: {Name: "two", Mode: Preempt}, 461 "example.com/gpu": {Name: "b_one", Mode: Preempt}, 462 }, 463 Requests: corev1.ResourceList{ 464 corev1.ResourceCPU: resource.MustParse("3000m"), 465 corev1.ResourceMemory: resource.MustParse("10Mi"), 466 "example.com/gpu": resource.MustParse("3"), 467 }, 468 Status: &Status{ 469 reasons: []string{ 470 "insufficient unused quota in cohort for cpu in flavor one, 1 more needed", 471 "insufficient unused quota in cohort for memory in flavor two, 5Mi more needed", 472 "insufficient unused quota in cohort for example.com/gpu in flavor b_one, 1 more needed", 473 }, 474 }, 475 Count: 1, 476 }}, 477 Usage: cache.FlavorResourceQuantities{ 478 "two": map[corev1.ResourceName]int64{ 479 corev1.ResourceCPU: 3000, 480 corev1.ResourceMemory: 10 * 1024 * 1024, 481 }, 482 "b_one": map[corev1.ResourceName]int64{ 483 "example.com/gpu": 3, 484 }, 485 }, 486 }, 487 }, 488 "multiple resources in a group, doesn't fit": { 489 wlPods: []kueue.PodSet{ 490 *utiltesting.MakePodSet("main", 1). 491 Request(corev1.ResourceCPU, "3"). 492 Request(corev1.ResourceMemory, "10Mi"). 493 Obj(), 494 }, 495 clusterQueue: cache.ClusterQueue{ 496 ResourceGroups: []cache.ResourceGroup{ 497 { 498 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourceMemory), 499 Flavors: []cache.FlavorQuotas{ 500 { 501 Name: "one", 502 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 503 corev1.ResourceCPU: {Nominal: 2000}, 504 corev1.ResourceMemory: {Nominal: utiltesting.Gi}, 505 }, 506 }, 507 { 508 Name: "two", 509 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 510 corev1.ResourceCPU: {Nominal: 4000}, 511 corev1.ResourceMemory: {Nominal: 5 * utiltesting.Mi}, 512 }, 513 }, 514 }, 515 }, 516 }, 517 }, 518 wantAssignment: Assignment{ 519 PodSets: []PodSetAssignment{{ 520 Name: "main", 521 Requests: corev1.ResourceList{ 522 corev1.ResourceCPU: resource.MustParse("3000m"), 523 corev1.ResourceMemory: resource.MustParse("10Mi"), 524 }, 525 Status: &Status{ 526 reasons: []string{ 527 "insufficient quota for cpu in flavor one in ClusterQueue", 528 "insufficient quota for memory in flavor two in ClusterQueue", 529 }, 530 }, 531 Count: 1, 532 }}, 533 Usage: cache.FlavorResourceQuantities{}, 534 }, 535 }, 536 "multiple flavors, fits while skipping tainted flavor": { 537 wlPods: []kueue.PodSet{ 538 *utiltesting.MakePodSet("main", 1). 539 Request(corev1.ResourceCPU, "3"). 540 Obj(), 541 }, 542 clusterQueue: cache.ClusterQueue{ 543 ResourceGroups: []cache.ResourceGroup{ 544 { 545 CoveredResources: sets.New(corev1.ResourceCPU), 546 Flavors: []cache.FlavorQuotas{ 547 { 548 Name: "tainted", 549 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 550 corev1.ResourceCPU: {Nominal: 4000}, 551 }, 552 }, 553 { 554 Name: "two", 555 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 556 corev1.ResourceCPU: {Nominal: 4000}, 557 }, 558 }, 559 }, 560 }, 561 }, 562 }, 563 wantRepMode: Fit, 564 wantAssignment: Assignment{ 565 PodSets: []PodSetAssignment{{ 566 Name: "main", 567 Flavors: ResourceAssignment{ 568 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 569 }, 570 Requests: corev1.ResourceList{ 571 corev1.ResourceCPU: resource.MustParse("3000m"), 572 }, 573 Count: 1, 574 }}, 575 Usage: cache.FlavorResourceQuantities{ 576 "two": map[corev1.ResourceName]int64{ 577 corev1.ResourceCPU: 3000, 578 }, 579 }, 580 }, 581 }, 582 "multiple flavors, skip missing ResourceFlavor": { 583 wlPods: []kueue.PodSet{ 584 *utiltesting.MakePodSet("main", 1). 585 Request(corev1.ResourceCPU, "3"). 586 Obj(), 587 }, 588 clusterQueue: cache.ClusterQueue{ 589 ResourceGroups: []cache.ResourceGroup{ 590 { 591 CoveredResources: sets.New(corev1.ResourceCPU), 592 Flavors: []cache.FlavorQuotas{ 593 { 594 Name: "nonexistent-flavor", 595 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 596 corev1.ResourceCPU: {Nominal: 4000}, 597 }, 598 }, 599 { 600 Name: "two", 601 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 602 corev1.ResourceCPU: {Nominal: 4000}, 603 }, 604 }, 605 }, 606 }, 607 }, 608 }, 609 wantRepMode: Fit, 610 wantAssignment: Assignment{ 611 PodSets: []PodSetAssignment{{ 612 Name: "main", 613 Flavors: ResourceAssignment{ 614 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 615 }, 616 Requests: corev1.ResourceList{ 617 corev1.ResourceCPU: resource.MustParse("3000m"), 618 }, 619 Count: 1, 620 }}, 621 Usage: cache.FlavorResourceQuantities{ 622 "two": map[corev1.ResourceName]int64{ 623 corev1.ResourceCPU: 3000, 624 }, 625 }, 626 }, 627 }, 628 "multiple flavors, fits a node selector": { 629 wlPods: []kueue.PodSet{ 630 { 631 Count: 1, 632 Name: "main", 633 Template: corev1.PodTemplateSpec{ 634 Spec: corev1.PodSpec{ 635 Containers: utiltesting.SingleContainerForRequest(map[corev1.ResourceName]string{ 636 corev1.ResourceCPU: "1", 637 }), 638 // ignored:foo should get ignored 639 NodeSelector: map[string]string{"type": "two", "ignored1": "foo"}, 640 Affinity: &corev1.Affinity{NodeAffinity: &corev1.NodeAffinity{ 641 RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ 642 NodeSelectorTerms: []corev1.NodeSelectorTerm{ 643 { 644 MatchExpressions: []corev1.NodeSelectorRequirement{ 645 { 646 // this expression should get ignored 647 Key: "ignored2", 648 Operator: corev1.NodeSelectorOpIn, 649 Values: []string{"bar"}, 650 }, 651 }, 652 }, 653 }, 654 }}, 655 }, 656 }, 657 }, 658 }, 659 }, 660 clusterQueue: cache.ClusterQueue{ 661 ResourceGroups: []cache.ResourceGroup{ 662 { 663 CoveredResources: sets.New(corev1.ResourceCPU), 664 Flavors: []cache.FlavorQuotas{ 665 { 666 Name: "non-existent", 667 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 668 corev1.ResourceCPU: {Nominal: 4000}, 669 }, 670 }, 671 { 672 Name: "one", 673 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 674 corev1.ResourceCPU: {Nominal: 4000}, 675 }, 676 }, 677 { 678 Name: "two", 679 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 680 corev1.ResourceCPU: {Nominal: 4000}, 681 }, 682 }, 683 }, 684 }, 685 }, 686 }, 687 wantRepMode: Fit, 688 wantAssignment: Assignment{ 689 PodSets: []PodSetAssignment{{ 690 Name: "main", 691 Flavors: ResourceAssignment{ 692 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 693 }, 694 Requests: corev1.ResourceList{ 695 corev1.ResourceCPU: resource.MustParse("1000m"), 696 }, 697 Count: 1, 698 }}, 699 Usage: cache.FlavorResourceQuantities{ 700 "two": map[corev1.ResourceName]int64{ 701 corev1.ResourceCPU: 1000, 702 }, 703 }, 704 }, 705 }, 706 "multiple flavors, fits with node affinity": { 707 wlPods: []kueue.PodSet{ 708 { 709 Count: 1, 710 Name: "main", 711 Template: corev1.PodTemplateSpec{ 712 Spec: corev1.PodSpec{ 713 Containers: utiltesting.SingleContainerForRequest(map[corev1.ResourceName]string{ 714 corev1.ResourceCPU: "1", 715 corev1.ResourceMemory: "1Mi", 716 }), 717 NodeSelector: map[string]string{"ignored1": "foo"}, 718 Affinity: &corev1.Affinity{NodeAffinity: &corev1.NodeAffinity{ 719 RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ 720 NodeSelectorTerms: []corev1.NodeSelectorTerm{ 721 { 722 MatchExpressions: []corev1.NodeSelectorRequirement{ 723 { 724 Key: "type", 725 Operator: corev1.NodeSelectorOpIn, 726 Values: []string{"two"}, 727 }, 728 }, 729 }, 730 }, 731 }}, 732 }, 733 }, 734 }, 735 }, 736 }, 737 clusterQueue: cache.ClusterQueue{ 738 ResourceGroups: []cache.ResourceGroup{ 739 { 740 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourceMemory), 741 Flavors: []cache.FlavorQuotas{ 742 { 743 Name: "one", 744 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 745 corev1.ResourceCPU: {Nominal: 4000}, 746 corev1.ResourceMemory: {Nominal: utiltesting.Gi}, 747 }, 748 }, 749 { 750 Name: "two", 751 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 752 corev1.ResourceCPU: {Nominal: 4000}, 753 corev1.ResourceMemory: {Nominal: utiltesting.Gi}, 754 }, 755 }, 756 }, 757 }, 758 }, 759 }, 760 wantRepMode: Fit, 761 wantAssignment: Assignment{ 762 PodSets: []PodSetAssignment{{ 763 Name: "main", 764 Flavors: ResourceAssignment{ 765 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 766 corev1.ResourceMemory: {Name: "two", Mode: Fit}, 767 }, 768 Requests: corev1.ResourceList{ 769 corev1.ResourceCPU: resource.MustParse("1000m"), 770 corev1.ResourceMemory: resource.MustParse("1Mi"), 771 }, 772 Count: 1, 773 }}, 774 Usage: cache.FlavorResourceQuantities{ 775 "two": map[corev1.ResourceName]int64{ 776 corev1.ResourceCPU: 1000, 777 corev1.ResourceMemory: 1 * 1024 * 1024, 778 }, 779 }, 780 }, 781 }, 782 "multiple flavors, node affinity fits any flavor": { 783 wlPods: []kueue.PodSet{ 784 { 785 Count: 1, 786 Name: "main", 787 Template: corev1.PodTemplateSpec{ 788 Spec: corev1.PodSpec{ 789 Containers: utiltesting.SingleContainerForRequest(map[corev1.ResourceName]string{ 790 corev1.ResourceCPU: "1", 791 }), 792 Affinity: &corev1.Affinity{NodeAffinity: &corev1.NodeAffinity{ 793 RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ 794 NodeSelectorTerms: []corev1.NodeSelectorTerm{ 795 { 796 MatchExpressions: []corev1.NodeSelectorRequirement{ 797 { 798 Key: "ignored2", 799 Operator: corev1.NodeSelectorOpIn, 800 Values: []string{"bar"}, 801 }, 802 }, 803 }, 804 { 805 MatchExpressions: []corev1.NodeSelectorRequirement{ 806 { 807 // although this terms selects two 808 // the first term practically matches 809 // any flavor; and since the terms 810 // are ORed, any flavor can be selected. 811 Key: "cpuType", 812 Operator: corev1.NodeSelectorOpIn, 813 Values: []string{"two"}, 814 }, 815 }, 816 }, 817 }, 818 }}, 819 }, 820 }, 821 }, 822 }, 823 }, 824 clusterQueue: cache.ClusterQueue{ 825 ResourceGroups: []cache.ResourceGroup{ 826 { 827 CoveredResources: sets.New(corev1.ResourceCPU), 828 Flavors: []cache.FlavorQuotas{ 829 { 830 Name: "one", 831 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 832 corev1.ResourceCPU: {Nominal: 4000}, 833 }, 834 }, 835 { 836 Name: "two", 837 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 838 corev1.ResourceCPU: {Nominal: 4000}, 839 }, 840 }, 841 }, 842 }, 843 }, 844 }, 845 wantRepMode: Fit, 846 wantAssignment: Assignment{ 847 PodSets: []PodSetAssignment{{ 848 Name: "main", 849 Flavors: ResourceAssignment{ 850 corev1.ResourceCPU: {Name: "one", Mode: Fit}, 851 }, 852 Requests: corev1.ResourceList{ 853 corev1.ResourceCPU: resource.MustParse("1000m"), 854 }, 855 Count: 1, 856 }}, 857 Usage: cache.FlavorResourceQuantities{ 858 "one": map[corev1.ResourceName]int64{ 859 corev1.ResourceCPU: 1000, 860 }, 861 }, 862 }, 863 }, 864 "multiple flavors, doesn't fit node affinity": { 865 wlPods: []kueue.PodSet{ 866 { 867 Count: 1, 868 Name: "main", 869 Template: corev1.PodTemplateSpec{ 870 Spec: corev1.PodSpec{ 871 Containers: utiltesting.SingleContainerForRequest(map[corev1.ResourceName]string{ 872 corev1.ResourceCPU: "1", 873 }), 874 Affinity: &corev1.Affinity{NodeAffinity: &corev1.NodeAffinity{ 875 RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ 876 NodeSelectorTerms: []corev1.NodeSelectorTerm{ 877 { 878 MatchExpressions: []corev1.NodeSelectorRequirement{ 879 { 880 Key: "type", 881 Operator: corev1.NodeSelectorOpIn, 882 Values: []string{"three"}, 883 }, 884 }, 885 }, 886 }, 887 }}, 888 }, 889 }, 890 }, 891 }, 892 }, 893 clusterQueue: cache.ClusterQueue{ 894 ResourceGroups: []cache.ResourceGroup{ 895 { 896 CoveredResources: sets.New(corev1.ResourceCPU), 897 Flavors: []cache.FlavorQuotas{ 898 { 899 Name: "one", 900 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 901 corev1.ResourceCPU: {Nominal: 4000}, 902 }, 903 }, 904 { 905 Name: "two", 906 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 907 corev1.ResourceCPU: {Nominal: 4000}, 908 }, 909 }, 910 }, 911 }, 912 }, 913 }, 914 wantAssignment: Assignment{ 915 PodSets: []PodSetAssignment{{ 916 Name: "main", 917 Requests: corev1.ResourceList{ 918 corev1.ResourceCPU: resource.MustParse("1000m"), 919 }, 920 Status: &Status{ 921 reasons: []string{ 922 "flavor one doesn't match node affinity", 923 "flavor two doesn't match node affinity", 924 }, 925 }, 926 Count: 1, 927 }}, 928 Usage: cache.FlavorResourceQuantities{}, 929 }, 930 }, 931 "multiple specs, fit different flavors": { 932 wlPods: []kueue.PodSet{ 933 *utiltesting.MakePodSet("driver", 1). 934 Request(corev1.ResourceCPU, "5"). 935 Obj(), 936 *utiltesting.MakePodSet("worker", 1). 937 Request(corev1.ResourceCPU, "3"). 938 Obj(), 939 }, 940 clusterQueue: cache.ClusterQueue{ 941 ResourceGroups: []cache.ResourceGroup{ 942 { 943 CoveredResources: sets.New(corev1.ResourceCPU), 944 Flavors: []cache.FlavorQuotas{ 945 { 946 Name: "one", 947 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 948 corev1.ResourceCPU: {Nominal: 4000}, 949 }, 950 }, 951 { 952 Name: "two", 953 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 954 corev1.ResourceCPU: {Nominal: 10_000}, 955 }, 956 }, 957 }, 958 }, 959 }, 960 }, 961 wantRepMode: Fit, 962 wantAssignment: Assignment{ 963 PodSets: []PodSetAssignment{ 964 { 965 Name: "driver", 966 Flavors: ResourceAssignment{ 967 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 968 }, 969 Requests: corev1.ResourceList{ 970 corev1.ResourceCPU: resource.MustParse("5000m"), 971 }, 972 Count: 1, 973 }, 974 { 975 Name: "worker", 976 Flavors: ResourceAssignment{ 977 corev1.ResourceCPU: {Name: "one", Mode: Fit}, 978 }, 979 Requests: corev1.ResourceList{ 980 corev1.ResourceCPU: resource.MustParse("3000m"), 981 }, 982 Count: 1, 983 }, 984 }, 985 Usage: cache.FlavorResourceQuantities{ 986 "one": map[corev1.ResourceName]int64{ 987 corev1.ResourceCPU: 3000, 988 }, 989 "two": map[corev1.ResourceName]int64{ 990 corev1.ResourceCPU: 5000, 991 }, 992 }, 993 }, 994 }, 995 "multiple specs, fits borrowing": { 996 wlPods: []kueue.PodSet{ 997 *utiltesting.MakePodSet("driver", 1). 998 Request(corev1.ResourceCPU, "4"). 999 Request(corev1.ResourceMemory, "1Gi"). 1000 Obj(), 1001 *utiltesting.MakePodSet("worker", 1). 1002 Request(corev1.ResourceCPU, "6"). 1003 Request(corev1.ResourceMemory, "4Gi"). 1004 Obj(), 1005 }, 1006 clusterQueue: cache.ClusterQueue{ 1007 ResourceGroups: []cache.ResourceGroup{{ 1008 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourceMemory), 1009 Flavors: []cache.FlavorQuotas{{ 1010 Name: "default", 1011 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1012 corev1.ResourceCPU: {Nominal: 2000, BorrowingLimit: ptr.To[int64](98_000)}, 1013 corev1.ResourceMemory: {Nominal: 2 * utiltesting.Gi}, 1014 }, 1015 }}, 1016 }}, 1017 Cohort: &cache.Cohort{ 1018 RequestableResources: cache.FlavorResourceQuantities{ 1019 "default": { 1020 corev1.ResourceCPU: 200_000, 1021 corev1.ResourceMemory: 200 * utiltesting.Gi, 1022 }, 1023 }, 1024 }, 1025 }, 1026 wantRepMode: Fit, 1027 wantAssignment: Assignment{ 1028 PodSets: []PodSetAssignment{ 1029 { 1030 Name: "driver", 1031 Flavors: ResourceAssignment{ 1032 corev1.ResourceCPU: {Name: "default", Mode: Fit}, 1033 corev1.ResourceMemory: {Name: "default", Mode: Fit}, 1034 }, 1035 Requests: corev1.ResourceList{ 1036 corev1.ResourceCPU: resource.MustParse("4000m"), 1037 corev1.ResourceMemory: resource.MustParse("1Gi"), 1038 }, 1039 Count: 1, 1040 }, 1041 { 1042 Name: "worker", 1043 Flavors: ResourceAssignment{ 1044 corev1.ResourceCPU: {Name: "default", Mode: Fit}, 1045 corev1.ResourceMemory: {Name: "default", Mode: Fit}, 1046 }, 1047 Requests: corev1.ResourceList{ 1048 corev1.ResourceCPU: resource.MustParse("6000m"), 1049 corev1.ResourceMemory: resource.MustParse("4Gi"), 1050 }, 1051 Count: 1, 1052 }, 1053 }, 1054 Borrowing: true, 1055 Usage: cache.FlavorResourceQuantities{ 1056 "default": map[corev1.ResourceName]int64{ 1057 corev1.ResourceCPU: 10000, 1058 corev1.ResourceMemory: 5 * 1024 * 1024 * 1024, 1059 }, 1060 }, 1061 }, 1062 }, 1063 "not enough space to borrow": { 1064 wlPods: []kueue.PodSet{ 1065 *utiltesting.MakePodSet("main", 1). 1066 Request(corev1.ResourceCPU, "2"). 1067 Obj(), 1068 }, 1069 clusterQueue: cache.ClusterQueue{ 1070 ResourceGroups: []cache.ResourceGroup{{ 1071 CoveredResources: sets.New(corev1.ResourceCPU), 1072 Flavors: []cache.FlavorQuotas{{ 1073 Name: "one", 1074 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1075 corev1.ResourceCPU: {Nominal: 1000}, 1076 }, 1077 }}, 1078 }}, 1079 Cohort: &cache.Cohort{ 1080 RequestableResources: cache.FlavorResourceQuantities{ 1081 "one": {corev1.ResourceCPU: 10_000}, 1082 }, 1083 Usage: cache.FlavorResourceQuantities{ 1084 "one": {corev1.ResourceCPU: 9_000}, 1085 }, 1086 }, 1087 }, 1088 wantAssignment: Assignment{ 1089 PodSets: []PodSetAssignment{{ 1090 Name: "main", 1091 Requests: corev1.ResourceList{ 1092 corev1.ResourceCPU: resource.MustParse("2000m"), 1093 }, 1094 Status: &Status{ 1095 reasons: []string{"insufficient unused quota in cohort for cpu in flavor one, 1 more needed"}, 1096 }, 1097 Count: 1, 1098 }}, 1099 Usage: cache.FlavorResourceQuantities{}, 1100 }, 1101 }, 1102 "past max, but can preempt in ClusterQueue": { 1103 wlPods: []kueue.PodSet{ 1104 *utiltesting.MakePodSet("main", 1). 1105 Request(corev1.ResourceCPU, "2"). 1106 Obj(), 1107 }, 1108 clusterQueue: cache.ClusterQueue{ 1109 ResourceGroups: []cache.ResourceGroup{{ 1110 CoveredResources: sets.New(corev1.ResourceCPU), 1111 Flavors: []cache.FlavorQuotas{{ 1112 Name: "one", 1113 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1114 corev1.ResourceCPU: {Nominal: 2000, BorrowingLimit: ptr.To[int64](8_000)}, 1115 }, 1116 }}, 1117 }}, 1118 Usage: cache.FlavorResourceQuantities{ 1119 "one": {corev1.ResourceCPU: 9_000}, 1120 }, 1121 Cohort: &cache.Cohort{ 1122 RequestableResources: cache.FlavorResourceQuantities{ 1123 "one": {corev1.ResourceCPU: 100_000}, 1124 }, 1125 Usage: cache.FlavorResourceQuantities{ 1126 "one": {corev1.ResourceCPU: 9_000}, 1127 }, 1128 }, 1129 }, 1130 wantRepMode: Preempt, 1131 wantAssignment: Assignment{ 1132 PodSets: []PodSetAssignment{{ 1133 Name: "main", 1134 Flavors: ResourceAssignment{ 1135 corev1.ResourceCPU: {Name: "one", Mode: Preempt}, 1136 }, 1137 Requests: corev1.ResourceList{ 1138 corev1.ResourceCPU: resource.MustParse("2000m"), 1139 }, 1140 1141 Status: &Status{ 1142 reasons: []string{"borrowing limit for cpu in flavor one exceeded"}, 1143 }, 1144 Count: 1, 1145 }}, 1146 Usage: cache.FlavorResourceQuantities{ 1147 "one": map[corev1.ResourceName]int64{ 1148 corev1.ResourceCPU: 2000, 1149 }, 1150 }, 1151 }, 1152 }, 1153 "past min, but can preempt in ClusterQueue": { 1154 wlPods: []kueue.PodSet{ 1155 *utiltesting.MakePodSet("main", 1). 1156 Request(corev1.ResourceCPU, "2"). 1157 Obj(), 1158 }, 1159 clusterQueue: cache.ClusterQueue{ 1160 ResourceGroups: []cache.ResourceGroup{{ 1161 CoveredResources: sets.New(corev1.ResourceCPU), 1162 Flavors: []cache.FlavorQuotas{{ 1163 Name: "one", 1164 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1165 corev1.ResourceCPU: {Nominal: 2000}, 1166 }, 1167 }}, 1168 }}, 1169 Usage: cache.FlavorResourceQuantities{ 1170 "one": {corev1.ResourceCPU: 1_000}, 1171 }, 1172 }, 1173 wantRepMode: Preempt, 1174 wantAssignment: Assignment{ 1175 PodSets: []PodSetAssignment{{ 1176 Name: "main", 1177 Flavors: ResourceAssignment{ 1178 corev1.ResourceCPU: {Name: "one", Mode: Preempt}, 1179 }, 1180 Requests: corev1.ResourceList{ 1181 corev1.ResourceCPU: resource.MustParse("2000m"), 1182 }, 1183 Status: &Status{ 1184 reasons: []string{"insufficient unused quota for cpu in flavor one, 1 more needed"}, 1185 }, 1186 Count: 1, 1187 }}, 1188 Usage: cache.FlavorResourceQuantities{ 1189 "one": map[corev1.ResourceName]int64{ 1190 corev1.ResourceCPU: 2000, 1191 }, 1192 }, 1193 }, 1194 }, 1195 "past min, but can preempt in cohort and ClusterQueue": { 1196 wlPods: []kueue.PodSet{ 1197 *utiltesting.MakePodSet("main", 1). 1198 Request(corev1.ResourceCPU, "2"). 1199 Obj(), 1200 }, 1201 clusterQueue: cache.ClusterQueue{ 1202 ResourceGroups: []cache.ResourceGroup{{ 1203 CoveredResources: sets.New(corev1.ResourceCPU), 1204 Flavors: []cache.FlavorQuotas{{ 1205 Name: "one", 1206 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1207 corev1.ResourceCPU: {Nominal: 3000}, 1208 }, 1209 }}, 1210 }}, 1211 Usage: cache.FlavorResourceQuantities{ 1212 "one": {corev1.ResourceCPU: 2_000}, 1213 }, 1214 Cohort: &cache.Cohort{ 1215 RequestableResources: cache.FlavorResourceQuantities{ 1216 "one": {corev1.ResourceCPU: 10_000}, 1217 }, 1218 Usage: cache.FlavorResourceQuantities{ 1219 "one": {corev1.ResourceCPU: 10_000}, 1220 }, 1221 }, 1222 }, 1223 wantRepMode: Preempt, 1224 wantAssignment: Assignment{ 1225 PodSets: []PodSetAssignment{{ 1226 Name: "main", 1227 Flavors: ResourceAssignment{ 1228 corev1.ResourceCPU: {Name: "one", Mode: Preempt}, 1229 }, 1230 Requests: corev1.ResourceList{ 1231 corev1.ResourceCPU: resource.MustParse("2000m"), 1232 }, 1233 Status: &Status{ 1234 reasons: []string{"insufficient unused quota in cohort for cpu in flavor one, 2 more needed"}, 1235 }, 1236 Count: 1, 1237 }}, 1238 Usage: cache.FlavorResourceQuantities{ 1239 "one": map[corev1.ResourceName]int64{ 1240 corev1.ResourceCPU: 2000, 1241 }, 1242 }, 1243 }, 1244 }, 1245 "can only preempt flavors that match affinity": { 1246 wlPods: []kueue.PodSet{ 1247 { 1248 Count: 1, 1249 Name: "main", 1250 Template: corev1.PodTemplateSpec{ 1251 Spec: corev1.PodSpec{ 1252 Containers: utiltesting.SingleContainerForRequest(map[corev1.ResourceName]string{ 1253 corev1.ResourceCPU: "2", 1254 }), 1255 NodeSelector: map[string]string{"type": "two"}, 1256 }, 1257 }, 1258 }, 1259 }, 1260 clusterQueue: cache.ClusterQueue{ 1261 ResourceGroups: []cache.ResourceGroup{{ 1262 CoveredResources: sets.New(corev1.ResourceCPU), 1263 Flavors: []cache.FlavorQuotas{ 1264 { 1265 Name: "one", 1266 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1267 corev1.ResourceCPU: {Nominal: 4000}, 1268 }, 1269 }, 1270 { 1271 Name: "two", 1272 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1273 corev1.ResourceCPU: {Nominal: 4000}, 1274 }, 1275 }, 1276 }, 1277 }}, 1278 Usage: cache.FlavorResourceQuantities{ 1279 "one": {corev1.ResourceCPU: 3000}, 1280 "two": {corev1.ResourceCPU: 3000}, 1281 }, 1282 }, 1283 wantRepMode: Preempt, 1284 wantAssignment: Assignment{ 1285 PodSets: []PodSetAssignment{{ 1286 Name: "main", 1287 Flavors: ResourceAssignment{ 1288 corev1.ResourceCPU: {Name: "two", Mode: Preempt}, 1289 }, 1290 Requests: corev1.ResourceList{ 1291 corev1.ResourceCPU: resource.MustParse("2000m"), 1292 }, 1293 Status: &Status{ 1294 reasons: []string{ 1295 "flavor one doesn't match node affinity", 1296 "insufficient unused quota for cpu in flavor two, 1 more needed", 1297 }, 1298 }, 1299 Count: 1, 1300 }}, 1301 Usage: cache.FlavorResourceQuantities{ 1302 "two": map[corev1.ResourceName]int64{ 1303 corev1.ResourceCPU: 2000, 1304 }, 1305 }, 1306 }, 1307 }, 1308 "each podset requires preemption on a different flavor": { 1309 wlPods: []kueue.PodSet{ 1310 *utiltesting.MakePodSet("launcher", 1). 1311 Request(corev1.ResourceCPU, "2"). 1312 Obj(), 1313 *utiltesting.MakePodSet("workers", 10). 1314 Request(corev1.ResourceCPU, "1"). 1315 Toleration(corev1.Toleration{ 1316 Key: "instance", 1317 Operator: corev1.TolerationOpEqual, 1318 Value: "spot", 1319 Effect: corev1.TaintEffectNoSchedule, 1320 }). 1321 Obj(), 1322 }, 1323 clusterQueue: cache.ClusterQueue{ 1324 ResourceGroups: []cache.ResourceGroup{{ 1325 CoveredResources: sets.New(corev1.ResourceCPU), 1326 Flavors: []cache.FlavorQuotas{ 1327 { 1328 Name: "one", 1329 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1330 corev1.ResourceCPU: {Nominal: 4000}, 1331 }, 1332 }, 1333 { 1334 Name: "tainted", 1335 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1336 corev1.ResourceCPU: {Nominal: 10_000}, 1337 }, 1338 }, 1339 }, 1340 }}, 1341 Usage: cache.FlavorResourceQuantities{ 1342 "one": {corev1.ResourceCPU: 3000}, 1343 "tainted": {corev1.ResourceCPU: 3000}, 1344 }, 1345 }, 1346 wantRepMode: Preempt, 1347 wantAssignment: Assignment{ 1348 PodSets: []PodSetAssignment{ 1349 { 1350 Name: "launcher", 1351 Flavors: ResourceAssignment{ 1352 corev1.ResourceCPU: {Name: "one", Mode: Preempt}, 1353 }, 1354 Requests: corev1.ResourceList{ 1355 corev1.ResourceCPU: resource.MustParse("2000m"), 1356 }, 1357 Status: &Status{ 1358 reasons: []string{ 1359 "insufficient unused quota for cpu in flavor one, 1 more needed", 1360 "untolerated taint {instance spot NoSchedule <nil>} in flavor tainted", 1361 }, 1362 }, 1363 Count: 1, 1364 }, 1365 { 1366 Name: "workers", 1367 Flavors: ResourceAssignment{ 1368 corev1.ResourceCPU: {Name: "tainted", Mode: Preempt}, 1369 }, 1370 Requests: corev1.ResourceList{ 1371 corev1.ResourceCPU: resource.MustParse("10000m"), 1372 }, 1373 Status: &Status{ 1374 reasons: []string{ 1375 "insufficient quota for cpu in flavor one in ClusterQueue", 1376 "insufficient unused quota for cpu in flavor tainted, 3 more needed", 1377 }, 1378 }, 1379 Count: 10, 1380 }, 1381 }, 1382 Usage: cache.FlavorResourceQuantities{ 1383 "one": map[corev1.ResourceName]int64{ 1384 corev1.ResourceCPU: 2000, 1385 }, 1386 "tainted": map[corev1.ResourceName]int64{ 1387 corev1.ResourceCPU: 10000, 1388 }, 1389 }, 1390 }, 1391 }, 1392 "resource not listed in clusterQueue": { 1393 wlPods: []kueue.PodSet{ 1394 *utiltesting.MakePodSet("main", 1). 1395 Request("example.com/gpu", "2"). 1396 Obj(), 1397 }, 1398 clusterQueue: cache.ClusterQueue{ 1399 ResourceGroups: []cache.ResourceGroup{{ 1400 CoveredResources: sets.New(corev1.ResourceCPU), 1401 Flavors: []cache.FlavorQuotas{{ 1402 Name: "one", 1403 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1404 corev1.ResourceCPU: {Nominal: 4000}, 1405 }, 1406 }}, 1407 }}, 1408 }, 1409 wantAssignment: Assignment{ 1410 PodSets: []PodSetAssignment{{ 1411 Name: "main", 1412 Requests: corev1.ResourceList{ 1413 "example.com/gpu": resource.MustParse("2"), 1414 }, 1415 Status: &Status{ 1416 reasons: []string{"resource example.com/gpu unavailable in ClusterQueue"}, 1417 }, 1418 Count: 1, 1419 }}, 1420 Usage: cache.FlavorResourceQuantities{}, 1421 }, 1422 }, 1423 "flavor not found": { 1424 wlPods: []kueue.PodSet{ 1425 *utiltesting.MakePodSet("main", 1). 1426 Request(corev1.ResourceCPU, "1"). 1427 Obj(), 1428 }, 1429 clusterQueue: cache.ClusterQueue{ 1430 ResourceGroups: []cache.ResourceGroup{{ 1431 CoveredResources: sets.New(corev1.ResourceCPU), 1432 Flavors: []cache.FlavorQuotas{{ 1433 Name: "nonexistent-flavor", 1434 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1435 corev1.ResourceCPU: {Nominal: 1000}, 1436 }, 1437 }}, 1438 }}, 1439 }, 1440 wantAssignment: Assignment{ 1441 PodSets: []PodSetAssignment{{ 1442 Name: "main", 1443 Requests: corev1.ResourceList{ 1444 corev1.ResourceCPU: resource.MustParse("1000m"), 1445 }, 1446 Status: &Status{ 1447 reasons: []string{"flavor nonexistent-flavor not found"}, 1448 }, 1449 Count: 1, 1450 }}, 1451 Usage: cache.FlavorResourceQuantities{}, 1452 }, 1453 }, 1454 "num pods fit": { 1455 wlPods: []kueue.PodSet{ 1456 *utiltesting.MakePodSet("main", 3). 1457 Request(corev1.ResourceCPU, "1"). 1458 Obj(), 1459 }, 1460 clusterQueue: cache.ClusterQueue{ 1461 ResourceGroups: []cache.ResourceGroup{{ 1462 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourcePods), 1463 Flavors: []cache.FlavorQuotas{{ 1464 Name: "default", 1465 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1466 corev1.ResourcePods: {Nominal: 3}, 1467 corev1.ResourceCPU: {Nominal: 10000}, 1468 }, 1469 }}, 1470 }}, 1471 }, 1472 wantAssignment: Assignment{ 1473 PodSets: []PodSetAssignment{{ 1474 Name: "main", 1475 Flavors: ResourceAssignment{ 1476 1477 corev1.ResourceCPU: &FlavorAssignment{Name: "default", Mode: Fit}, 1478 corev1.ResourcePods: &FlavorAssignment{Name: "default", Mode: Fit}, 1479 }, 1480 Requests: corev1.ResourceList{ 1481 corev1.ResourceCPU: resource.MustParse("3000m"), 1482 corev1.ResourcePods: resource.MustParse("3"), 1483 }, 1484 Count: 3, 1485 }}, 1486 Usage: cache.FlavorResourceQuantities{ 1487 "default": map[corev1.ResourceName]int64{ 1488 corev1.ResourcePods: 3, 1489 corev1.ResourceCPU: 3000, 1490 }, 1491 }, 1492 }, 1493 wantRepMode: Fit, 1494 }, 1495 "num pods don't fit": { 1496 wlPods: []kueue.PodSet{ 1497 *utiltesting.MakePodSet("main", 3). 1498 Request(corev1.ResourceCPU, "1"). 1499 Obj(), 1500 }, 1501 clusterQueue: cache.ClusterQueue{ 1502 ResourceGroups: []cache.ResourceGroup{{ 1503 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourcePods), 1504 Flavors: []cache.FlavorQuotas{{ 1505 Name: "default", 1506 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1507 corev1.ResourcePods: {Nominal: 2}, 1508 corev1.ResourceCPU: {Nominal: 10000}, 1509 }, 1510 }}, 1511 }}, 1512 }, 1513 wantAssignment: Assignment{ 1514 PodSets: []PodSetAssignment{{ 1515 Name: "main", 1516 Requests: corev1.ResourceList{ 1517 corev1.ResourceCPU: resource.MustParse("3000m"), 1518 corev1.ResourcePods: resource.MustParse("3"), 1519 }, 1520 Status: &Status{ 1521 reasons: []string{fmt.Sprintf("insufficient quota for %s in flavor default in ClusterQueue", corev1.ResourcePods)}, 1522 }, 1523 Count: 3, 1524 }}, 1525 Usage: cache.FlavorResourceQuantities{}, 1526 }, 1527 }, 1528 "with reclaimable pods": { 1529 wlPods: []kueue.PodSet{ 1530 *utiltesting.MakePodSet("main", 5). 1531 Request(corev1.ResourceCPU, "1"). 1532 Obj(), 1533 }, 1534 wlReclaimablePods: []kueue.ReclaimablePod{ 1535 { 1536 Name: "main", 1537 Count: 2, 1538 }, 1539 }, 1540 clusterQueue: cache.ClusterQueue{ 1541 ResourceGroups: []cache.ResourceGroup{{ 1542 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourcePods), 1543 Flavors: []cache.FlavorQuotas{{ 1544 Name: "default", 1545 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1546 corev1.ResourcePods: {Nominal: 3}, 1547 corev1.ResourceCPU: {Nominal: 10000}, 1548 }, 1549 }}, 1550 }}, 1551 }, 1552 wantAssignment: Assignment{ 1553 PodSets: []PodSetAssignment{{ 1554 Name: "main", 1555 Flavors: ResourceAssignment{ 1556 1557 corev1.ResourceCPU: &FlavorAssignment{Name: "default", Mode: Fit}, 1558 corev1.ResourcePods: &FlavorAssignment{Name: "default", Mode: Fit}, 1559 }, 1560 Requests: corev1.ResourceList{ 1561 corev1.ResourceCPU: resource.MustParse("3000m"), 1562 corev1.ResourcePods: resource.MustParse("3"), 1563 }, 1564 Count: 3, 1565 }}, 1566 Usage: cache.FlavorResourceQuantities{ 1567 "default": map[corev1.ResourceName]int64{ 1568 corev1.ResourcePods: 3, 1569 corev1.ResourceCPU: 3000, 1570 }, 1571 }, 1572 }, 1573 wantRepMode: Fit, 1574 }, 1575 "preempt before try next flavor": { 1576 wlPods: []kueue.PodSet{ 1577 *utiltesting.MakePodSet("main", 1). 1578 Request(corev1.ResourceCPU, "9"). 1579 Obj(), 1580 }, 1581 clusterQueue: cache.ClusterQueue{ 1582 FlavorFungibility: kueue.FlavorFungibility{ 1583 WhenCanBorrow: kueue.Borrow, 1584 WhenCanPreempt: kueue.Preempt, 1585 }, 1586 ResourceGroups: []cache.ResourceGroup{{ 1587 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourcePods), 1588 Flavors: []cache.FlavorQuotas{{ 1589 Name: "one", 1590 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1591 corev1.ResourcePods: {Nominal: 10}, 1592 corev1.ResourceCPU: {Nominal: 10000}, 1593 }, 1594 }, { 1595 Name: "two", 1596 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1597 corev1.ResourcePods: {Nominal: 10}, 1598 corev1.ResourceCPU: {Nominal: 10000}, 1599 }, 1600 }}, 1601 }}, 1602 Usage: cache.FlavorResourceQuantities{ 1603 "one": {corev1.ResourceCPU: 2000}, 1604 }, 1605 }, 1606 wantRepMode: Preempt, 1607 wantAssignment: Assignment{ 1608 PodSets: []PodSetAssignment{{ 1609 Name: "main", 1610 Flavors: ResourceAssignment{ 1611 corev1.ResourceCPU: {Name: "one", Mode: Preempt}, 1612 corev1.ResourcePods: {Name: "one", Mode: Fit}, 1613 }, 1614 Requests: corev1.ResourceList{ 1615 corev1.ResourceCPU: resource.MustParse("9000m"), 1616 corev1.ResourcePods: resource.MustParse("1"), 1617 }, 1618 Status: &Status{ 1619 reasons: []string{"insufficient unused quota for cpu in flavor one, 1 more needed"}, 1620 }, 1621 Count: 1, 1622 }}, 1623 Usage: cache.FlavorResourceQuantities{"one": {"cpu": 9000, "pods": 1}}, 1624 }, 1625 }, 1626 "preempt try next flavor": { 1627 wlPods: []kueue.PodSet{ 1628 *utiltesting.MakePodSet("main", 1). 1629 Request(corev1.ResourceCPU, "9"). 1630 Obj(), 1631 }, 1632 clusterQueue: cache.ClusterQueue{ 1633 FlavorFungibility: defaultFlavorFungibility, 1634 ResourceGroups: []cache.ResourceGroup{{ 1635 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourcePods), 1636 Flavors: []cache.FlavorQuotas{{ 1637 Name: "one", 1638 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1639 corev1.ResourcePods: {Nominal: 10}, 1640 corev1.ResourceCPU: {Nominal: 10000}, 1641 }, 1642 }, { 1643 Name: "two", 1644 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1645 corev1.ResourcePods: {Nominal: 10}, 1646 corev1.ResourceCPU: {Nominal: 10000}, 1647 }, 1648 }}, 1649 }}, 1650 Usage: cache.FlavorResourceQuantities{ 1651 "one": {corev1.ResourceCPU: 2000}, 1652 }, 1653 }, 1654 wantRepMode: Fit, 1655 wantAssignment: Assignment{ 1656 PodSets: []PodSetAssignment{{ 1657 Name: "main", 1658 Flavors: ResourceAssignment{ 1659 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 1660 corev1.ResourcePods: {Name: "two", Mode: Fit}, 1661 }, 1662 Requests: corev1.ResourceList{ 1663 corev1.ResourceCPU: resource.MustParse("9000m"), 1664 corev1.ResourcePods: resource.MustParse("1"), 1665 }, 1666 Count: 1, 1667 }}, 1668 Usage: cache.FlavorResourceQuantities{"two": {"cpu": 9000, "pods": 1}}, 1669 }, 1670 }, 1671 "borrow try next flavor, found the first flavor": { 1672 wlPods: []kueue.PodSet{ 1673 *utiltesting.MakePodSet("main", 1). 1674 Request(corev1.ResourceCPU, "9"). 1675 Obj(), 1676 }, 1677 clusterQueue: cache.ClusterQueue{ 1678 Cohort: &cache.Cohort{ 1679 Usage: cache.FlavorResourceQuantities{ 1680 "one": {corev1.ResourceCPU: 2000}, 1681 }, 1682 RequestableResources: cache.FlavorResourceQuantities{ 1683 "one": {corev1.ResourceCPU: 11000, corev1.ResourcePods: 10}, 1684 "two": {corev1.ResourceCPU: 1000, corev1.ResourcePods: 10}, 1685 }, 1686 }, 1687 FlavorFungibility: kueue.FlavorFungibility{ 1688 WhenCanBorrow: kueue.TryNextFlavor, 1689 WhenCanPreempt: kueue.TryNextFlavor, 1690 }, 1691 ResourceGroups: []cache.ResourceGroup{{ 1692 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourcePods), 1693 Flavors: []cache.FlavorQuotas{{ 1694 Name: "one", 1695 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1696 corev1.ResourcePods: {Nominal: 10}, 1697 corev1.ResourceCPU: {Nominal: 10000, BorrowingLimit: ptr.To[int64](1000)}, 1698 }, 1699 }, { 1700 Name: "two", 1701 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1702 corev1.ResourcePods: {Nominal: 10}, 1703 corev1.ResourceCPU: {Nominal: 1000}, 1704 }, 1705 }}, 1706 }}, 1707 Usage: cache.FlavorResourceQuantities{ 1708 "one": {corev1.ResourceCPU: 2000}, 1709 }, 1710 }, 1711 wantRepMode: Fit, 1712 wantAssignment: Assignment{ 1713 Borrowing: true, 1714 PodSets: []PodSetAssignment{{ 1715 Name: "main", 1716 Flavors: ResourceAssignment{ 1717 corev1.ResourceCPU: {Name: "one", Mode: Fit}, 1718 corev1.ResourcePods: {Name: "one", Mode: Fit}, 1719 }, 1720 Requests: corev1.ResourceList{ 1721 corev1.ResourceCPU: resource.MustParse("9000m"), 1722 corev1.ResourcePods: resource.MustParse("1"), 1723 }, 1724 Count: 1, 1725 }}, 1726 Usage: cache.FlavorResourceQuantities{ 1727 "one": {corev1.ResourceCPU: 9000, corev1.ResourcePods: 1}, 1728 }, 1729 }, 1730 }, 1731 "borrow try next flavor, found the second flavor": { 1732 wlPods: []kueue.PodSet{ 1733 *utiltesting.MakePodSet("main", 1). 1734 Request(corev1.ResourceCPU, "9"). 1735 Obj(), 1736 }, 1737 clusterQueue: cache.ClusterQueue{ 1738 Cohort: &cache.Cohort{ 1739 Usage: cache.FlavorResourceQuantities{ 1740 "one": {corev1.ResourceCPU: 2000}, 1741 }, 1742 RequestableResources: cache.FlavorResourceQuantities{ 1743 "one": {corev1.ResourceCPU: 11000, corev1.ResourcePods: 10}, 1744 "two": {corev1.ResourceCPU: 10000, corev1.ResourcePods: 10}, 1745 }, 1746 }, 1747 FlavorFungibility: kueue.FlavorFungibility{ 1748 WhenCanBorrow: kueue.TryNextFlavor, 1749 WhenCanPreempt: kueue.TryNextFlavor, 1750 }, 1751 ResourceGroups: []cache.ResourceGroup{{ 1752 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourcePods), 1753 Flavors: []cache.FlavorQuotas{{ 1754 Name: "one", 1755 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1756 corev1.ResourcePods: {Nominal: 10}, 1757 corev1.ResourceCPU: {Nominal: 10000, BorrowingLimit: ptr.To[int64](1000)}, 1758 }, 1759 }, { 1760 Name: "two", 1761 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1762 corev1.ResourcePods: {Nominal: 10}, 1763 corev1.ResourceCPU: {Nominal: 10000}, 1764 }, 1765 }}, 1766 }}, 1767 Usage: cache.FlavorResourceQuantities{ 1768 "one": {corev1.ResourceCPU: 2000}, 1769 }, 1770 }, 1771 wantRepMode: Fit, 1772 wantAssignment: Assignment{ 1773 PodSets: []PodSetAssignment{{ 1774 Name: "main", 1775 Flavors: ResourceAssignment{ 1776 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 1777 corev1.ResourcePods: {Name: "two", Mode: Fit}, 1778 }, 1779 Requests: corev1.ResourceList{ 1780 corev1.ResourceCPU: resource.MustParse("9000m"), 1781 corev1.ResourcePods: resource.MustParse("1"), 1782 }, 1783 Count: 1, 1784 }}, 1785 Usage: cache.FlavorResourceQuantities{ 1786 "two": {corev1.ResourceCPU: 9000, corev1.ResourcePods: 1}, 1787 }, 1788 }, 1789 }, 1790 "borrow before try next flavor": { 1791 wlPods: []kueue.PodSet{ 1792 *utiltesting.MakePodSet("main", 1). 1793 Request(corev1.ResourceCPU, "9"). 1794 Obj(), 1795 }, 1796 clusterQueue: cache.ClusterQueue{ 1797 Cohort: &cache.Cohort{ 1798 Usage: cache.FlavorResourceQuantities{ 1799 "one": {corev1.ResourceCPU: 2000}, 1800 }, 1801 RequestableResources: cache.FlavorResourceQuantities{ 1802 "one": {corev1.ResourceCPU: 11000, corev1.ResourcePods: 10}, 1803 "two": {corev1.ResourceCPU: 10000, corev1.ResourcePods: 10}, 1804 }, 1805 }, 1806 FlavorFungibility: defaultFlavorFungibility, 1807 ResourceGroups: []cache.ResourceGroup{{ 1808 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourcePods), 1809 Flavors: []cache.FlavorQuotas{{ 1810 Name: "one", 1811 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1812 corev1.ResourcePods: {Nominal: 10}, 1813 corev1.ResourceCPU: {Nominal: 10000, BorrowingLimit: ptr.To[int64](1000)}, 1814 }, 1815 }, { 1816 Name: "two", 1817 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1818 corev1.ResourcePods: {Nominal: 10}, 1819 corev1.ResourceCPU: {Nominal: 10000}, 1820 }, 1821 }}, 1822 }}, 1823 Usage: cache.FlavorResourceQuantities{ 1824 "one": {corev1.ResourceCPU: 2000}, 1825 }, 1826 }, 1827 wantRepMode: Fit, 1828 wantAssignment: Assignment{ 1829 Borrowing: true, 1830 PodSets: []PodSetAssignment{{ 1831 Name: "main", 1832 Flavors: ResourceAssignment{ 1833 corev1.ResourceCPU: {Name: "one", Mode: Fit}, 1834 corev1.ResourcePods: {Name: "one", Mode: Fit}, 1835 }, 1836 Requests: corev1.ResourceList{ 1837 corev1.ResourceCPU: resource.MustParse("9000m"), 1838 corev1.ResourcePods: resource.MustParse("1"), 1839 }, 1840 Count: 1, 1841 }}, 1842 Usage: cache.FlavorResourceQuantities{"one": {"cpu": 9000, "pods": 1}}, 1843 }, 1844 }, 1845 "when borrowing while preemption is needed for flavor one; WhenCanBorrow=Borrow": { 1846 wlPods: []kueue.PodSet{ 1847 *utiltesting.MakePodSet("main", 1). 1848 Request(corev1.ResourceCPU, "12"). 1849 Obj(), 1850 }, 1851 clusterQueue: cache.ClusterQueue{ 1852 Preemption: kueue.ClusterQueuePreemption{ 1853 ReclaimWithinCohort: kueue.PreemptionPolicyLowerPriority, 1854 BorrowWithinCohort: &kueue.BorrowWithinCohort{ 1855 Policy: kueue.BorrowWithinCohortPolicyLowerPriority, 1856 }, 1857 }, 1858 Cohort: &cache.Cohort{ 1859 Usage: cache.FlavorResourceQuantities{ 1860 "one": {corev1.ResourceCPU: 10000}, 1861 }, 1862 RequestableResources: cache.FlavorResourceQuantities{ 1863 "one": {corev1.ResourceCPU: 12000}, 1864 "two": {corev1.ResourceCPU: 12000}, 1865 }, 1866 }, 1867 FlavorFungibility: kueue.FlavorFungibility{ 1868 WhenCanBorrow: kueue.Borrow, 1869 WhenCanPreempt: kueue.Preempt, 1870 }, 1871 ResourceGroups: []cache.ResourceGroup{{ 1872 CoveredResources: sets.New(corev1.ResourceCPU), 1873 Flavors: []cache.FlavorQuotas{{ 1874 Name: "one", 1875 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1876 corev1.ResourceCPU: {Nominal: 0, BorrowingLimit: ptr.To[int64](12000)}, 1877 }, 1878 }, { 1879 Name: "two", 1880 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1881 corev1.ResourceCPU: {Nominal: 12000}, 1882 }, 1883 }}, 1884 }}, 1885 }, 1886 wantRepMode: Preempt, 1887 wantAssignment: Assignment{ 1888 Borrowing: true, 1889 PodSets: []PodSetAssignment{{ 1890 Name: "main", 1891 Flavors: ResourceAssignment{ 1892 corev1.ResourceCPU: {Name: "one", Mode: Preempt}, 1893 }, 1894 Status: &Status{ 1895 reasons: []string{"insufficient unused quota in cohort for cpu in flavor one, 10 more needed"}, 1896 }, 1897 Requests: corev1.ResourceList{ 1898 corev1.ResourceCPU: resource.MustParse("12000m"), 1899 }, 1900 Count: 1, 1901 }}, 1902 Usage: cache.FlavorResourceQuantities{ 1903 "one": {corev1.ResourceCPU: 12000}, 1904 }, 1905 }, 1906 }, 1907 "when borrowing while preemption is needed for flavor one, no borrowingLimit; WhenCanBorrow=Borrow": { 1908 wlPods: []kueue.PodSet{ 1909 *utiltesting.MakePodSet("main", 1). 1910 Request(corev1.ResourceCPU, "12"). 1911 Obj(), 1912 }, 1913 clusterQueue: cache.ClusterQueue{ 1914 Preemption: kueue.ClusterQueuePreemption{ 1915 ReclaimWithinCohort: kueue.PreemptionPolicyLowerPriority, 1916 BorrowWithinCohort: &kueue.BorrowWithinCohort{ 1917 Policy: kueue.BorrowWithinCohortPolicyLowerPriority, 1918 }, 1919 }, 1920 Cohort: &cache.Cohort{ 1921 Usage: cache.FlavorResourceQuantities{ 1922 "one": {corev1.ResourceCPU: 10000}, 1923 }, 1924 RequestableResources: cache.FlavorResourceQuantities{ 1925 "one": {corev1.ResourceCPU: 12000}, 1926 "two": {corev1.ResourceCPU: 12000}, 1927 }, 1928 }, 1929 FlavorFungibility: kueue.FlavorFungibility{ 1930 WhenCanBorrow: kueue.Borrow, 1931 WhenCanPreempt: kueue.Preempt, 1932 }, 1933 ResourceGroups: []cache.ResourceGroup{{ 1934 CoveredResources: sets.New(corev1.ResourceCPU), 1935 Flavors: []cache.FlavorQuotas{{ 1936 Name: "one", 1937 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1938 corev1.ResourceCPU: {Nominal: 0}, 1939 }, 1940 }, { 1941 Name: "two", 1942 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 1943 corev1.ResourceCPU: {Nominal: 12000}, 1944 }, 1945 }}, 1946 }}, 1947 }, 1948 wantRepMode: Preempt, 1949 wantAssignment: Assignment{ 1950 Borrowing: true, 1951 PodSets: []PodSetAssignment{{ 1952 Name: "main", 1953 Flavors: ResourceAssignment{ 1954 corev1.ResourceCPU: {Name: "one", Mode: Preempt}, 1955 }, 1956 Status: &Status{ 1957 reasons: []string{"insufficient unused quota in cohort for cpu in flavor one, 10 more needed"}, 1958 }, 1959 Requests: corev1.ResourceList{ 1960 corev1.ResourceCPU: resource.MustParse("12000m"), 1961 }, 1962 Count: 1, 1963 }}, 1964 Usage: cache.FlavorResourceQuantities{ 1965 "one": {corev1.ResourceCPU: 12000}, 1966 }, 1967 }, 1968 }, 1969 "when borrowing while preemption is needed for flavor one; WhenCanBorrow=TryNextFlavor": { 1970 wlPods: []kueue.PodSet{ 1971 *utiltesting.MakePodSet("main", 1). 1972 Request(corev1.ResourceCPU, "12"). 1973 Obj(), 1974 }, 1975 clusterQueue: cache.ClusterQueue{ 1976 Preemption: kueue.ClusterQueuePreemption{ 1977 ReclaimWithinCohort: kueue.PreemptionPolicyLowerPriority, 1978 BorrowWithinCohort: &kueue.BorrowWithinCohort{ 1979 Policy: kueue.BorrowWithinCohortPolicyLowerPriority, 1980 }, 1981 }, 1982 Cohort: &cache.Cohort{ 1983 Usage: cache.FlavorResourceQuantities{ 1984 "one": {corev1.ResourceCPU: 10000}, 1985 }, 1986 RequestableResources: cache.FlavorResourceQuantities{ 1987 "one": {corev1.ResourceCPU: 12000}, 1988 "two": {corev1.ResourceCPU: 12000}, 1989 }, 1990 }, 1991 FlavorFungibility: kueue.FlavorFungibility{ 1992 WhenCanBorrow: kueue.TryNextFlavor, 1993 WhenCanPreempt: kueue.Preempt, 1994 }, 1995 ResourceGroups: []cache.ResourceGroup{{ 1996 CoveredResources: sets.New(corev1.ResourceCPU), 1997 Flavors: []cache.FlavorQuotas{{ 1998 Name: "one", 1999 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 2000 corev1.ResourceCPU: {Nominal: 0, BorrowingLimit: ptr.To[int64](12000)}, 2001 }, 2002 }, { 2003 Name: "two", 2004 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 2005 corev1.ResourceCPU: {Nominal: 12000}, 2006 }, 2007 }}, 2008 }}, 2009 }, 2010 wantRepMode: Fit, 2011 wantAssignment: Assignment{ 2012 PodSets: []PodSetAssignment{{ 2013 Name: "main", 2014 Flavors: ResourceAssignment{ 2015 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 2016 }, 2017 Requests: corev1.ResourceList{ 2018 corev1.ResourceCPU: resource.MustParse("12000m"), 2019 }, 2020 Count: 1, 2021 }}, 2022 Usage: cache.FlavorResourceQuantities{ 2023 "two": {corev1.ResourceCPU: 12000}, 2024 }, 2025 }, 2026 }, 2027 "when borrowing while preemption is needed, but borrowingLimit exceeds the quota available in the cohort": { 2028 wlPods: []kueue.PodSet{ 2029 *utiltesting.MakePodSet("main", 1). 2030 Request(corev1.ResourceCPU, "12"). 2031 Obj(), 2032 }, 2033 clusterQueue: cache.ClusterQueue{ 2034 Preemption: kueue.ClusterQueuePreemption{ 2035 ReclaimWithinCohort: kueue.PreemptionPolicyLowerPriority, 2036 BorrowWithinCohort: &kueue.BorrowWithinCohort{ 2037 Policy: kueue.BorrowWithinCohortPolicyLowerPriority, 2038 }, 2039 }, 2040 Cohort: &cache.Cohort{ 2041 Usage: cache.FlavorResourceQuantities{ 2042 "one": {corev1.ResourceCPU: 10000}, 2043 }, 2044 RequestableResources: cache.FlavorResourceQuantities{ 2045 // below the borrowingLimit required to admit 2046 "one": {corev1.ResourceCPU: 11000}, 2047 }, 2048 }, 2049 ResourceGroups: []cache.ResourceGroup{{ 2050 CoveredResources: sets.New(corev1.ResourceCPU), 2051 Flavors: []cache.FlavorQuotas{{ 2052 Name: "one", 2053 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 2054 corev1.ResourceCPU: {Nominal: 0, BorrowingLimit: ptr.To[int64](12000)}, 2055 }, 2056 }}, 2057 }}, 2058 }, 2059 wantRepMode: NoFit, 2060 wantAssignment: Assignment{ 2061 Usage: cache.FlavorResourceQuantities{}, 2062 PodSets: []PodSetAssignment{ 2063 { 2064 Name: "main", 2065 Status: &Status{ 2066 reasons: []string{"insufficient unused quota in cohort for cpu in flavor one, 11 more needed"}, 2067 }, 2068 Requests: corev1.ResourceList{ 2069 corev1.ResourceCPU: resource.MustParse("12000m"), 2070 }, 2071 Count: 1, 2072 }, 2073 }, 2074 }, 2075 }, 2076 "lend try next flavor, found the second flavor": { 2077 wlPods: []kueue.PodSet{ 2078 *utiltesting.MakePodSet("main", 1). 2079 Request(corev1.ResourceCPU, "9"). 2080 Obj(), 2081 }, 2082 clusterQueue: cache.ClusterQueue{ 2083 Cohort: &cache.Cohort{ 2084 Usage: cache.FlavorResourceQuantities{ 2085 "one": {corev1.ResourceCPU: 2000}, 2086 }, 2087 RequestableResources: cache.FlavorResourceQuantities{ 2088 "one": {corev1.ResourceCPU: 11000, corev1.ResourcePods: 10}, 2089 "two": {corev1.ResourceCPU: 10000, corev1.ResourcePods: 10}, 2090 }, 2091 }, 2092 FlavorFungibility: kueue.FlavorFungibility{ 2093 WhenCanBorrow: kueue.TryNextFlavor, 2094 WhenCanPreempt: kueue.TryNextFlavor, 2095 }, 2096 ResourceGroups: []cache.ResourceGroup{{ 2097 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourcePods), 2098 Flavors: []cache.FlavorQuotas{{ 2099 Name: "one", 2100 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 2101 corev1.ResourcePods: {Nominal: 10}, 2102 corev1.ResourceCPU: {Nominal: 10_000, LendingLimit: ptr.To[int64](1000)}, 2103 }, 2104 }, { 2105 Name: "two", 2106 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 2107 corev1.ResourcePods: {Nominal: 10}, 2108 corev1.ResourceCPU: {Nominal: 10_000, LendingLimit: ptr.To[int64](0)}, 2109 }, 2110 }}, 2111 }}, 2112 Usage: cache.FlavorResourceQuantities{ 2113 "one": {corev1.ResourceCPU: 2000}, 2114 }, 2115 GuaranteedQuota: cache.FlavorResourceQuantities{ 2116 "one": { 2117 "cpu": 9, 2118 }, 2119 "two": { 2120 "cpu": 10, 2121 }, 2122 }, 2123 }, 2124 wantRepMode: Fit, 2125 wantAssignment: Assignment{ 2126 PodSets: []PodSetAssignment{{ 2127 Name: "main", 2128 Flavors: ResourceAssignment{ 2129 corev1.ResourceCPU: {Name: "two", Mode: Fit}, 2130 corev1.ResourcePods: {Name: "two", Mode: Fit}, 2131 }, 2132 Requests: corev1.ResourceList{ 2133 corev1.ResourceCPU: resource.MustParse("9000m"), 2134 corev1.ResourcePods: resource.MustParse("1"), 2135 }, 2136 Count: 1, 2137 }}, 2138 Usage: cache.FlavorResourceQuantities{ 2139 "two": {corev1.ResourceCPU: 9000, corev1.ResourcePods: 1}, 2140 }, 2141 }, 2142 enableLendingLimit: true, 2143 }, 2144 "lend try next flavor, found the first flavor": { 2145 wlPods: []kueue.PodSet{ 2146 *utiltesting.MakePodSet("main", 1). 2147 Request(corev1.ResourceCPU, "9"). 2148 Obj(), 2149 }, 2150 clusterQueue: cache.ClusterQueue{ 2151 Cohort: &cache.Cohort{ 2152 Usage: cache.FlavorResourceQuantities{ 2153 "one": {corev1.ResourceCPU: 2000}, 2154 }, 2155 RequestableResources: cache.FlavorResourceQuantities{ 2156 "one": {corev1.ResourceCPU: 11000, corev1.ResourcePods: 10}, 2157 "two": {corev1.ResourceCPU: 1000, corev1.ResourcePods: 10}, 2158 }, 2159 }, 2160 FlavorFungibility: kueue.FlavorFungibility{ 2161 WhenCanBorrow: kueue.TryNextFlavor, 2162 WhenCanPreempt: kueue.TryNextFlavor, 2163 }, 2164 ResourceGroups: []cache.ResourceGroup{{ 2165 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourcePods), 2166 Flavors: []cache.FlavorQuotas{{ 2167 Name: "one", 2168 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 2169 corev1.ResourcePods: {Nominal: 10}, 2170 corev1.ResourceCPU: {Nominal: 10_000, LendingLimit: ptr.To[int64](1000)}, 2171 }, 2172 }, { 2173 Name: "two", 2174 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 2175 corev1.ResourcePods: {Nominal: 10}, 2176 corev1.ResourceCPU: {Nominal: 1_000, LendingLimit: ptr.To[int64](0)}, 2177 }, 2178 }}, 2179 }}, 2180 Usage: cache.FlavorResourceQuantities{ 2181 "one": {corev1.ResourceCPU: 2000}, 2182 }, 2183 GuaranteedQuota: cache.FlavorResourceQuantities{ 2184 "one": { 2185 "cpu": 9, 2186 }, 2187 "two": { 2188 "cpu": 1, 2189 }, 2190 }, 2191 }, 2192 wantRepMode: Fit, 2193 wantAssignment: Assignment{ 2194 PodSets: []PodSetAssignment{{ 2195 Name: "main", 2196 Flavors: ResourceAssignment{ 2197 corev1.ResourceCPU: {Name: "one", Mode: Fit}, 2198 corev1.ResourcePods: {Name: "one", Mode: Fit}, 2199 }, 2200 Requests: corev1.ResourceList{ 2201 corev1.ResourceCPU: resource.MustParse("9000m"), 2202 corev1.ResourcePods: resource.MustParse("1"), 2203 }, 2204 Count: 1, 2205 }}, 2206 Borrowing: true, 2207 Usage: cache.FlavorResourceQuantities{ 2208 "one": {corev1.ResourceCPU: 9000, corev1.ResourcePods: 1}, 2209 }, 2210 }, 2211 enableLendingLimit: true, 2212 }, 2213 "lendingLimit exceeded, but can preempt in cohort and ClusterQueue": { 2214 wlPods: []kueue.PodSet{ 2215 *utiltesting.MakePodSet("main", 1). 2216 Request(corev1.ResourceCPU, "9"). 2217 Obj(), 2218 }, 2219 clusterQueue: cache.ClusterQueue{ 2220 Cohort: &cache.Cohort{ 2221 Usage: cache.FlavorResourceQuantities{ 2222 "one": {corev1.ResourceCPU: 2000}, 2223 }, 2224 RequestableResources: cache.FlavorResourceQuantities{ 2225 "one": {corev1.ResourceCPU: 10000, corev1.ResourcePods: 10}, 2226 }, 2227 }, 2228 FlavorFungibility: defaultFlavorFungibility, 2229 ResourceGroups: []cache.ResourceGroup{{ 2230 CoveredResources: sets.New(corev1.ResourceCPU, corev1.ResourcePods), 2231 Flavors: []cache.FlavorQuotas{{ 2232 Name: "one", 2233 Resources: map[corev1.ResourceName]*cache.ResourceQuota{ 2234 corev1.ResourcePods: {Nominal: 10}, 2235 corev1.ResourceCPU: {Nominal: 10_000, LendingLimit: ptr.To[int64](0)}, 2236 }, 2237 }}, 2238 }}, 2239 Usage: cache.FlavorResourceQuantities{ 2240 "one": {corev1.ResourceCPU: 2000}, 2241 }, 2242 }, 2243 wantRepMode: Preempt, 2244 wantAssignment: Assignment{ 2245 PodSets: []PodSetAssignment{{ 2246 Name: "main", 2247 Flavors: ResourceAssignment{ 2248 corev1.ResourceCPU: {Name: "one", Mode: Preempt}, 2249 corev1.ResourcePods: {Name: "one", Mode: Fit}, 2250 }, 2251 Requests: corev1.ResourceList{ 2252 corev1.ResourceCPU: resource.MustParse("9000m"), 2253 corev1.ResourcePods: resource.MustParse("1"), 2254 }, 2255 Status: &Status{ 2256 reasons: []string{"insufficient unused quota in cohort for cpu in flavor one, 1 more needed"}, 2257 }, 2258 Count: 1, 2259 }}, 2260 Usage: cache.FlavorResourceQuantities{ 2261 "one": {corev1.ResourceCPU: 9000, corev1.ResourcePods: 1}, 2262 }, 2263 }, 2264 enableLendingLimit: true, 2265 }, 2266 } 2267 for name, tc := range cases { 2268 t.Run(name, func(t *testing.T) { 2269 defer features.SetFeatureGateDuringTest(t, features.LendingLimit, tc.enableLendingLimit)() 2270 log := testr.NewWithOptions(t, testr.Options{ 2271 Verbosity: 2, 2272 }) 2273 wlInfo := workload.NewInfo(&kueue.Workload{ 2274 Spec: kueue.WorkloadSpec{ 2275 PodSets: tc.wlPods, 2276 }, 2277 Status: kueue.WorkloadStatus{ 2278 ReclaimablePods: tc.wlReclaimablePods, 2279 }, 2280 }) 2281 if tc.clusterQueue.FlavorFungibility.WhenCanBorrow == "" { 2282 tc.clusterQueue.FlavorFungibility.WhenCanBorrow = kueue.Borrow 2283 } 2284 if tc.clusterQueue.FlavorFungibility.WhenCanPreempt == "" { 2285 tc.clusterQueue.FlavorFungibility.WhenCanPreempt = kueue.TryNextFlavor 2286 } 2287 tc.clusterQueue.UpdateWithFlavors(resourceFlavors) 2288 tc.clusterQueue.UpdateRGByResource() 2289 assignment := AssignFlavors(log, wlInfo, resourceFlavors, &tc.clusterQueue, nil) 2290 if repMode := assignment.RepresentativeMode(); repMode != tc.wantRepMode { 2291 t.Errorf("e.assignFlavors(_).RepresentativeMode()=%s, want %s", repMode, tc.wantRepMode) 2292 } 2293 2294 if diff := cmp.Diff(tc.wantAssignment, assignment, cmpopts.IgnoreUnexported(Assignment{}, FlavorAssignment{}), cmpopts.IgnoreFields(Assignment{}, "LastState"), cmpopts.IgnoreFields(FlavorAssignment{}, "TriedFlavorIdx")); diff != "" { 2295 t.Errorf("Unexpected assignment (-want,+got):\n%s", diff) 2296 } 2297 }) 2298 } 2299 } 2300 2301 func TestLastAssignmentOutdated(t *testing.T) { 2302 type args struct { 2303 wl *workload.Info 2304 cq *cache.ClusterQueue 2305 } 2306 tests := []struct { 2307 name string 2308 args args 2309 want bool 2310 }{ 2311 { 2312 name: "Cluster queue allocatableResourceIncreasedGen increased", 2313 args: args{ 2314 wl: &workload.Info{ 2315 LastAssignment: &workload.AssigmentClusterQueueState{ 2316 ClusterQueueGeneration: 0, 2317 }, 2318 }, 2319 cq: &cache.ClusterQueue{ 2320 Cohort: nil, 2321 AllocatableResourceGeneration: 1, 2322 }, 2323 }, 2324 want: true, 2325 }, 2326 { 2327 name: "Cohort allocatableResourceIncreasedGen increased", 2328 args: args{ 2329 wl: &workload.Info{ 2330 LastAssignment: &workload.AssigmentClusterQueueState{ 2331 ClusterQueueGeneration: 0, 2332 CohortGeneration: 0, 2333 }, 2334 }, 2335 cq: &cache.ClusterQueue{ 2336 Cohort: &cache.Cohort{ 2337 AllocatableResourceGeneration: 1, 2338 }, 2339 AllocatableResourceGeneration: 0, 2340 }, 2341 }, 2342 want: true, 2343 }, 2344 { 2345 name: "AllocatableResourceGeneration not increased", 2346 args: args{ 2347 wl: &workload.Info{ 2348 LastAssignment: &workload.AssigmentClusterQueueState{ 2349 ClusterQueueGeneration: 0, 2350 CohortGeneration: 0, 2351 }, 2352 }, 2353 cq: &cache.ClusterQueue{ 2354 Cohort: &cache.Cohort{ 2355 AllocatableResourceGeneration: 0, 2356 }, 2357 AllocatableResourceGeneration: 0, 2358 }, 2359 }, 2360 want: false, 2361 }, 2362 } 2363 for _, tt := range tests { 2364 t.Run(tt.name, func(t *testing.T) { 2365 if got := lastAssignmentOutdated(tt.args.wl, tt.args.cq); got != tt.want { 2366 t.Errorf("LastAssignmentOutdated() = %v, want %v", got, tt.want) 2367 } 2368 }) 2369 } 2370 }