Skip to content

Commit a72e24d

Browse files
KaiPilotBotjunis
andauthored
fix(api): allow PodGroup minMember of 0 (#2018)
Signed-off-by: lin121291 <4jp33f9e@gmail.com> Signed-off-by: junis <4jp33f9e@gmail.com> Co-authored-by: junis <4jp33f9e@gmail.com>
1 parent fd7bfe9 commit a72e24d

10 files changed

Lines changed: 93 additions & 15 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Fixed
2+
body: |-
3+
Allow PodGroup minMember and minSubGroup of 0 for workloads with no gang requirement

deployments/kai-scheduler/crds/scheduling.run.ai_podgroups.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,19 @@ spec:
5454
description: |-
5555
MinMember defines the minimal number of members to run the PodGroup;
5656
if there are not enough resources to start all required members, the scheduler will not start anyone.
57+
A value of 0 means no gang requirement: all pods are scheduled elastically (e.g. scale-to-zero workloads).
5758
Mutually exclusive with MinSubGroup.
5859
format: int32
59-
minimum: 1
60+
minimum: 0
6061
type: integer
6162
minSubGroup:
6263
description: |-
6364
MinSubGroup defines the minimal number of direct child SubGroups required for this PodGroup to be schedulable.
65+
A value of 0 means no gang requirement: all SubGroups are scheduled elastically.
6466
Only applicable when SubGroups are defined.
6567
Mutually exclusive with MinMember.
6668
format: int32
67-
minimum: 1
69+
minimum: 0
6870
type: integer
6971
preemptibility:
7072
description: |-

pkg/apis/scheduling/v2alpha2/podgroup_types.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,18 @@ import (
3535
type PodGroupSpec struct {
3636
// MinMember defines the minimal number of members to run the PodGroup;
3737
// if there are not enough resources to start all required members, the scheduler will not start anyone.
38+
// A value of 0 means no gang requirement: all pods are scheduled elastically (e.g. scale-to-zero workloads).
3839
// Mutually exclusive with MinSubGroup.
3940
// +kubebuilder:validation:Nullable
40-
// +kubebuilder:validation:Minimum=1
41+
// +kubebuilder:validation:Minimum=0
4142
MinMember *int32 `json:"minMember,omitempty" protobuf:"varint,1,opt,name=minMember"`
4243

4344
// MinSubGroup defines the minimal number of direct child SubGroups required for this PodGroup to be schedulable.
45+
// A value of 0 means no gang requirement: all SubGroups are scheduled elastically.
4446
// Only applicable when SubGroups are defined.
4547
// Mutually exclusive with MinMember.
4648
// +kubebuilder:validation:Optional
47-
// +kubebuilder:validation:Minimum=1
49+
// +kubebuilder:validation:Minimum=0
4850
MinSubGroup *int32 `json:"minSubGroup,omitempty"`
4951

5052
// Queue defines the queue to allocate resource for PodGroup; if queue does not exist,

pkg/apis/scheduling/v2alpha2/podgroup_webhook.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,6 @@ func validatePodGroupSpec(spec *PodGroupSpec) *validationErrors {
113113
}
114114

115115
if spec.MinSubGroup != nil {
116-
if *spec.MinSubGroup < 1 {
117-
validationErrors.minDefinitionErrors = append(validationErrors.minDefinitionErrors,
118-
&invalidMinSubGroupError{msg: "minSubGroup at the podgroup level must be equal to or greater than 1"})
119-
return validationErrors
120-
}
121116
rootCount := countRootSubGroups(spec.SubGroups)
122117
if int(*spec.MinSubGroup) > rootCount {
123118
validationErrors.minDefinitionErrors = append(validationErrors.minDefinitionErrors,

pkg/apis/scheduling/v2alpha2/podgroup_webhook_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,17 @@ func TestValidatePodGroupSpec(t *testing.T) {
249249
},
250250
want: nil,
251251
},
252+
{
253+
name: "Valid: minSubGroup of 0 (no gang requirement)",
254+
spec: PodGroupSpec{
255+
MinSubGroup: ptr.To(int32(0)),
256+
SubGroups: []SubGroup{
257+
{Name: "a", MinMember: ptr.To(int32(4))},
258+
{Name: "b", MinMember: ptr.To(int32(4))},
259+
},
260+
},
261+
want: nil,
262+
},
252263
{
253264
name: "Invalid: both minMember and minSubGroup set on PodGroup",
254265
spec: PodGroupSpec{

pkg/podgrouper/podgrouper/plugins/knative/knative_test.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package knative
55

66
import (
7-
"strconv"
87
"testing"
98

109
"github.com/stretchr/testify/assert"
@@ -113,6 +112,17 @@ func TestGetPodGroupMetadata(t *testing.T) {
113112
}
114113

115114
func TestGetPodGroupMetadata_MinScale(t *testing.T) {
115+
for minScale, expectedMinAvailable := range map[string]int32{
116+
"3": 3,
117+
"0": 0, // scale-to-zero: no gang requirement
118+
} {
119+
t.Run("min-scale="+minScale, func(t *testing.T) {
120+
testGetPodGroupMetadataMinScale(t, minScale, expectedMinAvailable)
121+
})
122+
}
123+
}
124+
125+
func testGetPodGroupMetadataMinScale(t *testing.T, minScale string, expectedMinAvailable int32) {
116126
service := &unstructured.Unstructured{
117127
Object: map[string]interface{}{
118128
"kind": "Service",
@@ -156,7 +166,7 @@ func TestGetPodGroupMetadata_MinScale(t *testing.T) {
156166
Namespace: "test_namespace",
157167
UID: "2",
158168
Annotations: map[string]string{
159-
"autoscaling.knative.dev/min-scale": "3",
169+
"autoscaling.knative.dev/min-scale": minScale,
160170
},
161171
},
162172
Spec: knative.RevisionSpec{},
@@ -197,7 +207,7 @@ func TestGetPodGroupMetadata_MinScale(t *testing.T) {
197207
assert.Equal(t, "pg-revision-2", metadata.Name)
198208
assert.Equal(t, constants.InferencePriorityClass, metadata.PriorityClassName)
199209
assert.Equal(t, "test_queue", metadata.Queue)
200-
assert.Equal(t, "3", strconv.Itoa(int(metadata.MinAvailable)))
210+
assert.Equal(t, expectedMinAvailable, metadata.MinAvailable)
201211
}
202212

203213
func TestGetPodGroupMetadataBackwardsCompatibility(t *testing.T) {

pkg/scheduler/api/podgroup_info/job_info.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ func (pgi *PodGroupInfo) setSubGroups(podGroup *enginev2alpha2.PodGroup) error {
284284
if defaultPodSet, found := pgi.PodSets[DefaultSubGroup]; found {
285285
minAvail := int32(1)
286286
if podGroup.Spec.MinMember != nil {
287-
minAvail = max(*podGroup.Spec.MinMember, 1)
287+
minAvail = *podGroup.Spec.MinMember
288288
}
289289
defaultPodSet.SetMinAvailable(minAvail)
290290
rootSubGroupSet.AddPodSet(defaultPodSet)

pkg/scheduler/api/podgroup_info/job_info_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,35 @@ func TestAddTaskInfoTracksInvalidSubGroupTask(t *testing.T) {
178178
assert.Contains(t, info.TasksFitErrors[task.UID].Error(), `missing-subgroup`)
179179
}
180180

181+
func TestSetPodGroupMinMember(t *testing.T) {
182+
tests := []struct {
183+
name string
184+
minMember *int32
185+
expectedMinAvail int32
186+
}{
187+
{"nil minMember defaults to 1", nil, 1},
188+
{"zero minMember is honored", ptr.To(int32(0)), 0},
189+
{"positive minMember is honored", ptr.To(int32(3)), 3},
190+
}
191+
for _, test := range tests {
192+
t.Run(test.name, func(t *testing.T) {
193+
info := NewPodGroupInfo("group-1")
194+
info.SetPodGroup(&enginev2alpha2.PodGroup{
195+
ObjectMeta: metav1.ObjectMeta{
196+
Name: "group-1",
197+
Namespace: "ns-1",
198+
},
199+
Spec: enginev2alpha2.PodGroupSpec{
200+
Queue: "queue-1",
201+
MinMember: test.minMember,
202+
},
203+
})
204+
205+
assert.Equal(t, test.expectedMinAvail, info.PodSets[DefaultSubGroup].GetMinAvailable())
206+
})
207+
}
208+
}
209+
181210
func TestDeleteTaskInfo(t *testing.T) {
182211
// case1
183212
case01_uid := common_info.PodGroupID("owner1")

test/e2e/suites/api/crds/podgroup/validations.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ var _ = Describe("MinSubGroup validation", Ordered, func() {
101101
Expect(warningCapture.Messages[0]).To(ContainSubstring("minSubGroup (5) exceeds the number of direct child SubGroups (4)"))
102102
})
103103

104-
It("should reject minSubGroup = 0", func(ctx context.Context) {
104+
It("should accept minSubGroup = 0", func(ctx context.Context) {
105105
pg := createMinSubGroupPodGroup(testCtx, ptr.To[int32](0), 0,
106106
[]schedulingv2alpha2.SubGroup{
107107
{Name: "prefill-0", MinMember: ptr.To[int32](8)},
@@ -110,7 +110,7 @@ var _ = Describe("MinSubGroup validation", Ordered, func() {
110110

111111
_, err := testCtx.KubeAiSchedClientset.SchedulingV2alpha2().PodGroups(pg.Namespace).Create(ctx,
112112
pg, metav1.CreateOptions{})
113-
Expect(err).ToNot(Succeed())
113+
Expect(err).To(Succeed())
114114
})
115115

116116
It("should reject SubGroup with both minMember and minSubGroup", func(ctx context.Context) {

test/e2e/suites/integrations/third_party/knative/knative_specs.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,32 @@ func DescribeKnativeSpecs() bool {
111111
Expect(podGroups.Items[0].Spec.MinMember).To(Equal(k8sptr.To(int32(1))),
112112
"Expected minmember of podgroup to be 1")
113113
})
114+
115+
It("knative service with scale-to-zero - minscale 0", func(ctx context.Context) {
116+
namespace := queue.GetConnectedNamespaceToQueue(testCtx.Queues[0])
117+
serviceName := "knative-service-" + utils.GenerateRandomK8sName(10)
118+
119+
knativeService := GetKnativeServiceObject(serviceName, namespace, testCtx.Queues[0].Name)
120+
121+
knativeService.Spec.ConfigurationSpec.Template.Annotations[minScaleAnnotation] = "0"
122+
123+
Expect(testCtx.ControllerClient.Create(ctx, knativeService)).To(Succeed())
124+
defer func() {
125+
Expect(testCtx.ControllerClient.Delete(ctx, knativeService)).To(Succeed())
126+
}()
127+
128+
// Knative still creates the initial pod on deployment even with min-scale 0
129+
pods := GetServicePods(ctx, testCtx, serviceName, namespace, 1)
130+
131+
wait.ForPodsScheduled(ctx, testCtx.ControllerClient, namespace, pods)
132+
133+
var podGroups v2alpha2.PodGroupList
134+
Expect(testCtx.ControllerClient.List(ctx, &podGroups, client.InNamespace(namespace))).To(Succeed())
135+
Expect(len(podGroups.Items)).To(Equal(1),
136+
"Expected one podgroup for the revision")
137+
Expect(podGroups.Items[0].Spec.MinMember).To(Equal(k8sptr.To(int32(0))),
138+
"Expected minmember of podgroup to be 0")
139+
})
114140
})
115141
})
116142
}

0 commit comments

Comments
 (0)