Skip to content

Commit dfc6e55

Browse files
committed
Add PDB for kai scheduler
Signed-off-by: dttung2905 <ttdao.2015@accountancy.smu.edu.sg>
1 parent 55d8aba commit dfc6e55

10 files changed

Lines changed: 256 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,40 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [Unreleased]
8+
9+
### Added
10+
- Added `global.resourceReservation.createNamespace` Helm value (default `true`) to allow disabling creation of the resource-reservation namespace, for embedding KAI in a parent chart that creates the namespace itself.
11+
- Added `global.resourceReservation.createServiceAccount` Helm value (default `true`) to allow disabling creation of the resource-reservation ServiceAccount, for embedding KAI in a parent chart that creates the ServiceAccount itself.
12+
- Added `defaultPriorityClasses.enabled` Helm value (default `true`) for installations that manage KAI PriorityClasses externally.
13+
- Added GitOps/ArgoCD install support ([guide](docs/gitops/README.md)): `kaiConfig.render` Helm value (default `false`) renders the `kai-config` Config CR inline as a tracked release resource (mutually exclusive with `kaiConfigDeployer.enabled`), `openshift` value (default `false`) forces OpenShift mode where `lookup` auto-detection is unavailable under offline rendering, and ArgoCD `PostDelete` hook and `Prune=false` annotations (requires ArgoCD >= 2.10). [#1794](https://github.com/kai-scheduler/KAI-Scheduler/issues/1794) [#1751](https://github.com/kai-scheduler/KAI-Scheduler/issues/1751)
14+
- Added **topology level aliases**: a `Topology` level may declare an `alias` (e.g. `rack`), usable in place of the raw node label key in a workload's `requiredTopologyLevel`/`preferredTopologyLevel`. Aliases are one-to-one (unique within the Topology and must not collide with a `nodeLabel`, enforced by a new Topology validating webhook) and may be edited freely (the `levels` immutability rule now freezes only the `nodeLabel` structure). When a level has no alias, behavior is unchanged and raw label keys keep working. [#1498](https://github.com/kai-scheduler/KAI-Scheduler/issues/1498)
15+
- Added support for configuring admission Pod Disruption Budget via Helm values (`admission.podDisruptionBudget`) [#1490](https://github.com/kai-scheduler/KAI-Scheduler/pull/1490) [dttung2905](https://github.com/dttung2905)
16+
- Added support for configuring scheduler Pod Disruption Budget via Helm values (`scheduler.podDisruptionBudget`) when running multiple replicas per scheduling shard
17+
18+
### Changed
19+
- Scenario search now skips re-simulating equivalent victim-set candidates that already failed simulation for the same pending job (reclaim, preempt, consolidation). Skipped candidates are recorded as `state="duplicate"` in `scenario_search_scenarios_total`. [#1719](https://github.com/kai-scheduler/KAI-Scheduler/issues/1719)
20+
- Removed unused `queuecontroller.certSecretName` and `admission.certSecretName` Helm values; webhook TLS secrets are created and managed by the operator (`queue-webhook-tls-secret`, `kai-admission-webhook-tls-secret`). [#1791](https://github.com/kai-scheduler/KAI-Scheduler/pull/1791) [dttung2905](https://github.com/dttung2905)
21+
- Podgrouper now preserves an existing PodGroup's topology constraint when the workload does not specify one, so an externally-assigned topology is not overwritten. Workload topology annotations still take precedence when present.
22+
23+
### Fixed
24+
- Scenario search no longer leaks a rejected scenario's victim nodes into the probe's feasible-node set: the solver now rolls back feasible-node additions after validator-rejected and errored simulations, not only after cleanly unsolved ones. [#1719](https://github.com/kai-scheduler/KAI-Scheduler/issues/1719)
25+
- Scoped the operator's informer cache for Pods, Leases and EndpointSlices to the KAI namespace and stripped managed fields from cached objects. Since v0.15.0 the operator cached every such object in the cluster, so its memory grew with cluster size and exceeded the default 256Mi limit on large clusters. [#1780](https://github.com/kai-scheduler/KAI-Scheduler/issues/1780)
26+
- Reduced transient scheduler allocations during large reclaim operations by comparing proportion queue state and cached resource vectors directly instead of repeatedly materializing resource maps.
27+
- Scheduler now exits on 401 Unauthorized API responses instead of retrying indefinitely with a stale ServiceAccount token. [#1817](https://github.com/kai-scheduler/KAI-Scheduler/issues/1817)
28+
- Reduced scheduler memory use during large reclaim operations by removing redundant per-job-pair min-runtime protection caching; effective min-runtime durations remain cached per queue pair. [#1808](https://github.com/kai-scheduler/KAI-Scheduler/issues/1808)
29+
- Fixed reclaim abandoning valid over-quota victims when an unrelated under-deserved queue appeared earlier in victim ordering. [#1750](https://github.com/kai-scheduler/KAI-Scheduler/issues/1750)
30+
- Restricted Helm post-delete cleanup to KAI operator-managed Deployments and preserved externally managed `kai-config` resources when `kaiConfigDeployer.enabled=false`.
31+
- Scheduler cache now filters terminal Pods at watch time to reduce memory use, while still watching Pods bound by other schedulers so their resource usage is counted in allocatable calculations. [#1645](https://github.com/kai-scheduler/KAI-Scheduler/issues/1645) [enoodle](https://github.com/enoodle)
32+
- Fix the MinNodeGPUMemoryMiB calculation in the scheduler. This affected allocations for fractional pod requesting gpu "gpu-memory". [#1792](https://github.com/kai-scheduler/KAI-Scheduler/issues/1792) [davidLif](https://github.com/davidLif)
33+
- Use the maximum gpu size ine the cluster rather then the minimum when checking a potential overLimit or isNonPreemptebleOverquota for a pod. [#1792](https://github.com/kai-scheduler/KAI-Scheduler/issues/1792) [davidLif](https://github.com/davidLif)
34+
- Reduced allocation churn in the scheduler hot path: cached `Schedulable()` result as a package-level singleton and lazily formatted `logNodeSetsPluginResult` node names only when verbose logging is enabled.
35+
- Block NaN value for fraction in the pod admission [#1798](https://github.com/kai-scheduler/KAI-Scheduler/issues/1798) [davidLif](https://github.com/davidLif)
36+
- In the fractional admission checks, check that the fractional value can be parsed as a quantity. [#1798](https://github.com/kai-scheduler/KAI-Scheduler/issues/1798) [davidLif](https://github.com/davidLif)
37+
- Podgrouper now rejects negative PyTorch replica indexes and LWS worker indexes, and caps the number of subgroups created for block-level segmentation at 10000 to avoid unbounded PodGroup fan-out. [davidLif](https://github.com/davidLif)
38+
- Fixed GPU-sharing pods with dotted pod names generating invalid ConfigMap-backed volume names. Volume names are now sanitized to valid DNS labels while preserving original ConfigMap references used for shared-GPU injection.
39+
40+
741
## [v0.16.0] - 2026-06-24
842

943
### Added

deployments/kai-scheduler/templates/_helpers.tpl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,13 @@ spec:
283283
affinity:
284284
{{- toYaml .Values.scheduler.affinity | nindent 8 }}
285285
{{- end }}
286+
podDisruptionBudget:
287+
{{- if hasKey .Values.scheduler.podDisruptionBudget "enabled" }}
288+
enabled: {{ .Values.scheduler.podDisruptionBudget.enabled }}
289+
{{- end }}
290+
{{- if hasKey .Values.scheduler.podDisruptionBudget "maxUnavailable" }}
291+
maxUnavailable: {{ .Values.scheduler.podDisruptionBudget.maxUnavailable }}
292+
{{- end }}
286293
{{- if and .Values.scheduler.ports .Values.scheduler.ports.metricsPort }}
287294
schedulerService:
288295
port: {{ .Values.scheduler.ports.metricsPort }}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright 2025 NVIDIA CORPORATION
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
suite: scheduler podDisruptionBudget in kai-config
5+
6+
set:
7+
kaiConfigDeployer:
8+
enabled: false
9+
kaiConfig:
10+
render: true
11+
12+
templates:
13+
- kai-config.yaml
14+
tests:
15+
- it: renders scheduler podDisruptionBudget from values
16+
set:
17+
scheduler.podDisruptionBudget:
18+
enabled: true
19+
maxUnavailable: 1
20+
asserts:
21+
- equal:
22+
path: spec.scheduler.service.podDisruptionBudget.enabled
23+
value: true
24+
- equal:
25+
path: spec.scheduler.service.podDisruptionBudget.maxUnavailable
26+
value: 1
27+
28+
- it: renders disabled PDB without maxUnavailable when only enabled is set in test values
29+
set:
30+
scheduler.podDisruptionBudget:
31+
enabled: false
32+
asserts:
33+
- equal:
34+
path: spec.scheduler.service.podDisruptionBudget.enabled
35+
value: false

deployments/kai-scheduler/values.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ scheduler:
163163
actions: {}
164164
# Alpha/experimental scenario search time budgets. Values are Go duration strings.
165165
scenarioSearchBudgets: {}
166+
# PDB is created only when scheduler replicas > 1 (global.replicaCount / scheduler.replicas).
167+
# For HA: set operator.replicaCount: 2 (or override scheduler.replicas in Config).
168+
podDisruptionBudget:
169+
enabled: true
170+
maxUnavailable: 1
166171

167172
# defaultShard controls the chart-managed "default" SchedulingShard CR,
168173
# whose spec is populated from the scheduler.* values above.

pkg/operator/operands/common/common.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const (
3636
// PodDisruptionBudgetImplementedServices lists operand resource names with operator-side PDB creation.
3737
var PodDisruptionBudgetImplementedServices = map[string]struct{}{
3838
"admission": {},
39+
"scheduler": {},
3940
}
4041

4142
func PodDisruptionBudgetImplemented(serviceName string) bool {

pkg/operator/operands/common/common_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,9 +538,9 @@ var _ = Describe("PodDisruptionBudgetForKAIConfig", func() {
538538

539539
var _ = Describe("PodDisruptionBudgetImplementedServices", func() {
540540
It("only lists operands with operator-side PDB creation", func() {
541-
Expect(PodDisruptionBudgetImplementedServices).To(HaveLen(1))
541+
Expect(PodDisruptionBudgetImplementedServices).To(HaveLen(2))
542542
Expect(PodDisruptionBudgetImplemented("admission")).To(BeTrue())
543+
Expect(PodDisruptionBudgetImplemented("scheduler")).To(BeTrue())
543544
Expect(PodDisruptionBudgetImplemented("binder")).To(BeFalse())
544-
Expect(PodDisruptionBudgetImplemented("scheduler")).To(BeFalse())
545545
})
546546
})

pkg/operator/operands/scheduler/resources_for_shard.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,21 @@ func DeploymentName(config *kaiv1.Config, shard *kaiv1.SchedulingShard) string {
375375
return fmt.Sprintf("%s-%s", *config.Spec.Global.SchedulerName, shard.Name)
376376
}
377377

378+
func (s *SchedulerForShard) podDisruptionBudgetForShard(
379+
ctx context.Context, readerClient client.Reader,
380+
kaiConfig *kaiv1.Config, shard *kaiv1.SchedulingShard,
381+
) (client.Object, error) {
382+
config := kaiConfig.Spec.Scheduler
383+
return common.PodDisruptionBudgetForKAIConfig(
384+
ctx,
385+
readerClient,
386+
kaiConfig.Spec.Namespace,
387+
DeploymentName(kaiConfig, shard),
388+
config.Replicas,
389+
config.Service,
390+
)
391+
}
392+
378393
// serviceName for the per-shard scheduler Service.
379394
func serviceName(config *kaiv1.Config, shard *kaiv1.SchedulingShard) string {
380395
return fmt.Sprintf("%s-%s", *config.Spec.Global.SchedulerName, shard.Name)

pkg/operator/operands/scheduler/resources_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/kai-scheduler/KAI-scheduler/cmd/scheduler/app/options"
1717
kaiv1 "github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1"
18+
"github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1/common"
1819
kaiprometheus "github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1/prometheus"
1920
kaiv1qc "github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1/queue_controller"
2021
kaiv1scheduler "github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1/scheduler"
@@ -27,6 +28,7 @@ import (
2728
"github.com/stretchr/testify/require"
2829
appsv1 "k8s.io/api/apps/v1"
2930
corev1 "k8s.io/api/core/v1"
31+
policyv1 "k8s.io/api/policy/v1"
3032
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3133
"k8s.io/utils/ptr"
3234
"sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -1413,3 +1415,81 @@ func TestGetUsageDBConfig(t *testing.T) {
14131415
})
14141416
}
14151417
}
1418+
1419+
func TestPodDisruptionBudgetForShard(t *testing.T) {
1420+
ctx := context.Background()
1421+
client := fake.NewClientBuilder().Build()
1422+
1423+
shard := &kaiv1.SchedulingShard{
1424+
ObjectMeta: metav1.ObjectMeta{Name: "default"},
1425+
}
1426+
shard.Spec.SetDefaultsWhereNeeded()
1427+
1428+
tests := []struct {
1429+
name string
1430+
replicas int32
1431+
pdbEnabled bool
1432+
maxUnavailable int32
1433+
expectPDBCreation bool
1434+
}{
1435+
{
1436+
name: "skip PDB when replicas is one",
1437+
replicas: 1,
1438+
pdbEnabled: true,
1439+
maxUnavailable: 1,
1440+
expectPDBCreation: false,
1441+
},
1442+
{
1443+
name: "create PDB when replicas greater than one and enabled",
1444+
replicas: 2,
1445+
pdbEnabled: true,
1446+
maxUnavailable: 1,
1447+
expectPDBCreation: true,
1448+
},
1449+
{
1450+
name: "skip PDB when disabled",
1451+
replicas: 3,
1452+
pdbEnabled: false,
1453+
maxUnavailable: 1,
1454+
expectPDBCreation: false,
1455+
},
1456+
{
1457+
name: "custom maxUnavailable",
1458+
replicas: 2,
1459+
pdbEnabled: true,
1460+
maxUnavailable: 2,
1461+
expectPDBCreation: true,
1462+
},
1463+
}
1464+
1465+
for _, tt := range tests {
1466+
t.Run(tt.name, func(t *testing.T) {
1467+
config := &kaiv1.Config{}
1468+
config.Spec.SetDefaultsWhereNeeded()
1469+
config.Spec.Scheduler.Replicas = ptr.To(tt.replicas)
1470+
config.Spec.Scheduler.Service.PodDisruptionBudget = &common.PodDisruptionBudget{
1471+
Enabled: ptr.To(tt.pdbEnabled),
1472+
MaxUnavailable: ptr.To(tt.maxUnavailable),
1473+
}
1474+
1475+
s := NewSchedulerForShard(shard)
1476+
obj, err := s.podDisruptionBudgetForShard(ctx, client, config, shard)
1477+
require.NoError(t, err)
1478+
1479+
if !tt.expectPDBCreation {
1480+
assert.Nil(t, obj)
1481+
return
1482+
}
1483+
1484+
require.NotNil(t, obj)
1485+
pdb, ok := obj.(*policyv1.PodDisruptionBudget)
1486+
require.True(t, ok, "object should be PodDisruptionBudget")
1487+
assert.Equal(t, "kai-scheduler-default", pdb.Name)
1488+
assert.Equal(t, constants.DefaultKAINamespace, pdb.Namespace)
1489+
require.NotNil(t, pdb.Spec.MaxUnavailable)
1490+
assert.Equal(t, tt.maxUnavailable, pdb.Spec.MaxUnavailable.IntVal)
1491+
require.NotNil(t, pdb.Spec.Selector)
1492+
assert.Equal(t, "kai-scheduler-default", pdb.Spec.Selector.MatchLabels["app"])
1493+
})
1494+
}
1495+
}

pkg/operator/operands/scheduler/scheduler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func (s *SchedulerForShard) DesiredState(
5858
objects := []client.Object{}
5959
for _, resourceFunc := range []resourceForShard{
6060
s.deploymentForShard,
61+
s.podDisruptionBudgetForShard,
6162
s.configMapForShard,
6263
s.serviceForShard,
6364
s.endpointSliceForShard,

pkg/operator/operands/scheduler/scheduler_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111
. "github.com/onsi/gomega"
1212

1313
appsv1 "k8s.io/api/apps/v1"
14+
policyv1 "k8s.io/api/policy/v1"
1415

1516
kaiv1 "github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1"
17+
"github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1/common"
1618

1719
v1 "k8s.io/api/core/v1"
1820
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -130,6 +132,80 @@ var _ = Describe("Scheduler", func() {
130132
Expect(len(services.Items)).To(Equal(2))
131133
})
132134

135+
It("Should create one PDB per shard when HA and PDB enabled", func(ctx context.Context) {
136+
kaiConfig.Spec.Scheduler.Replicas = ptr.To(int32(2))
137+
kaiConfig.Spec.Scheduler.Service.PodDisruptionBudget = &common.PodDisruptionBudget{
138+
Enabled: ptr.To(true),
139+
MaxUnavailable: ptr.To(int32(1)),
140+
}
141+
142+
desiredState, err := schedulerOperandForShard.DesiredState(ctx, fakeClient, kaiConfig)
143+
Expect(err).To(BeNil())
144+
for _, obj := range desiredState {
145+
Expect(fakeClient.Create(ctx, obj)).To(Succeed())
146+
}
147+
148+
otherShard := &kaiv1.SchedulingShard{
149+
ObjectMeta: metav1.ObjectMeta{Name: "other"},
150+
}
151+
otherShard.Spec.SetDefaultsWhereNeeded()
152+
otherDesiredState, err := NewSchedulerForShard(otherShard).DesiredState(ctx, fakeClient, kaiConfig)
153+
Expect(err).To(BeNil())
154+
for _, obj := range otherDesiredState {
155+
Expect(fakeClient.Create(ctx, obj)).To(Succeed())
156+
}
157+
158+
pdbs := &policyv1.PodDisruptionBudgetList{}
159+
Expect(fakeClient.List(ctx, pdbs, client.InNamespace(kaiConfig.Spec.Namespace))).To(Succeed())
160+
Expect(pdbs.Items).To(HaveLen(2))
161+
pdbNames := []string{pdbs.Items[0].Name, pdbs.Items[1].Name}
162+
Expect(pdbNames).To(ConsistOf("kai-scheduler-default", "kai-scheduler-other"))
163+
})
164+
165+
Context("PodDisruptionBudget DesiredState", func() {
166+
It("includes PDB when HA and enabled", func(ctx context.Context) {
167+
kaiConfig.Spec.Scheduler.Replicas = ptr.To(int32(2))
168+
kaiConfig.Spec.Scheduler.Service.PodDisruptionBudget = &common.PodDisruptionBudget{
169+
Enabled: ptr.To(true),
170+
}
171+
172+
desiredState, err := schedulerOperandForShard.DesiredState(ctx, fakeClient, kaiConfig)
173+
Expect(err).To(BeNil())
174+
Expect(desiredState).To(HaveLen(5))
175+
176+
var pdbCount int
177+
for _, obj := range desiredState {
178+
if _, ok := obj.(*policyv1.PodDisruptionBudget); ok {
179+
pdbCount++
180+
}
181+
}
182+
Expect(pdbCount).To(Equal(1))
183+
})
184+
185+
It("omits PDB when HA but disabled", func(ctx context.Context) {
186+
kaiConfig.Spec.Scheduler.Replicas = ptr.To(int32(2))
187+
kaiConfig.Spec.Scheduler.Service.PodDisruptionBudget = &common.PodDisruptionBudget{
188+
Enabled: ptr.To(false),
189+
}
190+
191+
desiredState, err := schedulerOperandForShard.DesiredState(ctx, fakeClient, kaiConfig)
192+
Expect(err).To(BeNil())
193+
Expect(desiredState).To(HaveLen(4))
194+
195+
for _, obj := range desiredState {
196+
Expect(obj).NotTo(BeAssignableToTypeOf(&policyv1.PodDisruptionBudget{}))
197+
}
198+
})
199+
200+
It("returns empty desired state when scheduler disabled", func(ctx context.Context) {
201+
kaiConfig.Spec.Scheduler.Service.Enabled = ptr.To(false)
202+
203+
desiredState, err := schedulerOperandForShard.DesiredState(ctx, fakeClient, kaiConfig)
204+
Expect(err).To(BeNil())
205+
Expect(desiredState).To(BeEmpty())
206+
})
207+
})
208+
133209
Context("ConfigMap", func() {
134210
It("Should create configmap", func(ctx context.Context) {
135211
s := NewSchedulerForShard(shard)

0 commit comments

Comments
 (0)