Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions pkg/cache/tas_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2798,6 +2798,142 @@ func TestFindTopologyAssignment(t *testing.T) {
count: 1,
wantReason: "no requested topology level for slices: not-existing-topology-level",
},
"no topology for podset; host required for slices; BestFit": {
// b1
// |
// r1
// / | \
// x1:3, x2:3, x3:3
//
nodes: []corev1.Node{
*testingnode.MakeNode("b1-r1-x1").
Label(tasBlockLabel, "b1").
Label(tasRackLabel, "r1").
Label(corev1.LabelHostname, "x1").
StatusAllocatable(corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("3"),
corev1.ResourcePods: resource.MustParse("10"),
}).
Ready().
Obj(),
*testingnode.MakeNode("b1-r1-x2").
Label(tasBlockLabel, "b1").
Label(tasRackLabel, "r1").
Label(corev1.LabelHostname, "x2").
StatusAllocatable(corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("3"),
corev1.ResourcePods: resource.MustParse("10"),
}).
Ready().
Obj(),
*testingnode.MakeNode("b1-r1-x3").
Label(tasBlockLabel, "b1").
Label(tasRackLabel, "r1").
Label(corev1.LabelHostname, "x3").
StatusAllocatable(corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("3"),
corev1.ResourcePods: resource.MustParse("10"),
}).
Ready().
Obj(),
},
topologyRequest: &kueue.PodSetTopologyRequest{
PodSetSliceRequiredTopology: ptr.To(corev1.LabelHostname),
PodSetSliceSize: ptr.To(int32(2)),
},
levels: defaultThreeLevels,
requests: resources.Requests{
corev1.ResourceCPU: 1000,
},
count: 6,
wantAssignment: &kueue.TopologyAssignment{
Levels: defaultOneLevel,
Domains: []kueue.TopologyDomainAssignment{
{
Count: 2,
Values: []string{
"x3",
},
},
{
Count: 2,
Values: []string{
"x2",
},
},
{
Count: 2,
Values: []string{
"x1",
},
},
},
},
},
"no topology for podset; host required for slices; multiple blocks; BestFit": {
nodes: scatteredNodes,
topologyRequest: &kueue.PodSetTopologyRequest{
PodSetSliceRequiredTopology: ptr.To(corev1.LabelHostname),
PodSetSliceSize: ptr.To(int32(2)),
},
levels: defaultThreeLevels,
requests: resources.Requests{
corev1.ResourceCPU: 1000,
},
count: 6,
wantAssignment: &kueue.TopologyAssignment{
Levels: defaultOneLevel,
Domains: []kueue.TopologyDomainAssignment{
{
Count: 4,
Values: []string{
"x1",
},
},
{
Count: 2,
Values: []string{
"x4",
},
},
},
},
},
"no topology for podset; rack required for slices; multiple blocks; BestFit": {
nodes: defaultNodes,
topologyRequest: &kueue.PodSetTopologyRequest{
PodSetSliceRequiredTopology: ptr.To(tasRackLabel),
PodSetSliceSize: ptr.To(int32(2)),
},
levels: defaultThreeLevels,
requests: resources.Requests{
corev1.ResourceCPU: 1000,
},
count: 4,
wantAssignment: &kueue.TopologyAssignment{
Levels: defaultOneLevel,
Domains: []kueue.TopologyDomainAssignment{
{
Count: 1,
Values: []string{
"x2",
},
},
{
Count: 1,
Values: []string{
"x3",
},
},
{
Count: 2,
Values: []string{
"x6",
},
},
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
Expand Down
13 changes: 12 additions & 1 deletion pkg/cache/tas_flavor_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ func (s *TASFlavorSnapshot) lowestLevel() string {
return s.levelKeys[len(s.levelKeys)-1]
}

func (s *TASFlavorSnapshot) highestLevel() string {
return s.levelKeys[0]
}

// initialize prepares the topology tree structure. This structure holds
// for a given the list of topology domains with additional static and dynamic
// information. This function initializes the static information which
Expand Down Expand Up @@ -726,6 +730,9 @@ func isRequired(tr *kueue.PodSetTopologyRequest) bool {
}

func (s *TASFlavorSnapshot) levelKeyWithImpliedFallback(tasRequests *TASPodSetRequests) *string {
if isSliceTopologyOnlyRequest(tasRequests.PodSet.TopologyRequest) {
return ptr.To(s.highestLevel())
}
if key := s.levelKey(tasRequests.PodSet.TopologyRequest); key != nil {
return key
}
Expand Down Expand Up @@ -759,7 +766,11 @@ func (s *TASFlavorSnapshot) sliceLevelKeyWithDefault(tasRequests *TASPodSetReque
}

func isUnconstrained(tr *kueue.PodSetTopologyRequest, tasRequests *TASPodSetRequests) bool {
return (tr != nil && tr.Unconstrained != nil && *tr.Unconstrained) || tasRequests.Implied
return (tr != nil && tr.Unconstrained != nil && *tr.Unconstrained) || tasRequests.Implied || isSliceTopologyOnlyRequest(tr)
}

func isSliceTopologyOnlyRequest(tr *kueue.PodSetTopologyRequest) bool {
return tr != nil && tr.Required == nil && tr.Preferred == nil && tr.PodSetSliceRequiredTopology != nil
}

// findBestFitDomain finds an index of the first domain with the lowest
Expand Down
3 changes: 0 additions & 3 deletions pkg/controller/jobframework/tas_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,6 @@ func ValidateTASPodSetRequest(replicaPath *field.Path, replicaMetadata *metav1.O

// validate slice annotations
if sliceRequiredFound {
if !requiredFound && !preferredFound {
allErrs = append(allErrs, field.Forbidden(annotationsPath.Key(kueuealpha.PodSetSliceRequiredTopologyAnnotation), "can be used with required or preferred topology only"))
}
if !sliceSizeFound {
allErrs = append(allErrs, field.Required(annotationsPath.Key(kueuealpha.PodSetSliceSizeAnnotation), "slice size is required if slice topology is requested"))
}
Expand Down
17 changes: 6 additions & 11 deletions pkg/controller/jobs/job/job_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,15 +287,13 @@ func TestValidateCreate(t *testing.T) {
wantErr: nil,
},
{
name: "invalid topology request - unconstrained with slices defined",
name: "valid topology request - slice-only topology - unconstrained with slices defined",
job: testingutil.MakeJob("job", "default").
PodAnnotation(kueuealpha.PodSetUnconstrainedTopologyAnnotation, "true").
PodAnnotation(kueuealpha.PodSetSliceRequiredTopologyAnnotation, "cloud.com/block").
PodAnnotation(kueuealpha.PodSetSliceSizeAnnotation, "1").
Obj(),
wantErr: field.ErrorList{
field.Forbidden(replicaMetaPath.Child("annotations").Key("kueue.x-k8s.io/podset-slice-required-topology"), "can be used with required or preferred topology only"),
},
wantErr: nil,
},
{
name: "invalid topology request - slice requested without slice size",
Expand Down Expand Up @@ -351,14 +349,12 @@ func TestValidateCreate(t *testing.T) {
},
},
{
name: "invalid topology request - slice topology requested without podset topology",
name: "valid topology request - slice-only topology",
job: testingutil.MakeJob("job", "default").
PodAnnotation(kueuealpha.PodSetSliceRequiredTopologyAnnotation, "cloud.com/block").
PodAnnotation(kueuealpha.PodSetSliceSizeAnnotation, "1").
Obj(),
wantErr: field.ErrorList{
field.Forbidden(replicaMetaPath.Child("annotations").Key("kueue.x-k8s.io/podset-slice-required-topology"), "can be used with required or preferred topology only"),
},
wantErr: nil,
},
}

Expand Down Expand Up @@ -639,12 +635,11 @@ func TestValidateUpdate(t *testing.T) {
oldJob: testingutil.MakeJob("job", "default").
Obj(),
newJob: testingutil.MakeJob("job", "default").
PodAnnotation(kueuealpha.PodSetUnconstrainedTopologyAnnotation, "true").
PodAnnotation(kueuealpha.PodSetRequiredTopologyAnnotation, "cloud.com/block").
PodAnnotation(kueuealpha.PodSetSliceRequiredTopologyAnnotation, "cloud.com/block").
PodAnnotation(kueuealpha.PodSetSliceSizeAnnotation, "1").
Obj(),
wantErr: field.ErrorList{
field.Forbidden(replicaMetaPath.Child("annotations").Key("kueue.x-k8s.io/podset-slice-required-topology"), "can be used with required or preferred topology only"),
field.Required(replicaMetaPath.Child("annotations").Key("kueue.x-k8s.io/podset-slice-size"), "slice size is required if slice topology is requested"),
},
},
}
Expand Down