Skip to content

Commit fdb29bd

Browse files
committed
feat(operator): reflect GPU sharing readiness status
Signed-off-by: davidLif <davidshani12@gmail.com>
1 parent b983538 commit fdb29bd

10 files changed

Lines changed: 353 additions & 42 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Added
2+
body: |-
3+
Gate NvFractions on GPU sharing readiness

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,7 @@ spec:
13331333
description: |-
13341334
SelectedGPUGroups is the name of the selected GPU groups for fractional GPU resources.
13351335
Only if the RecievedResourceType is "Fraction"
1336+
Deprecated: Use SelectedFractionalGpuGroups instead
13361337
items:
13371338
type: string
13381339
type: array

deployments/kai-scheduler/templates/rbac/operator.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,14 @@ rules:
136136
- patch
137137
- update
138138
- watch
139+
- apiGroups:
140+
- gpu-sharing.kai.scheduler
141+
resources:
142+
- gpusharingconfigs
143+
verbs:
144+
- get
145+
- list
146+
- watch
139147
- apiGroups:
140148
- kai.scheduler
141149
resources:

docs/operator/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,49 @@ spec:
7171

7272
- [Scheduling Shards](./scheduling-shards.md) - Advanced cluster partitioning
7373

74+
## Status Conditions
75+
76+
The KAI operator reports installation health on `Config.status.conditions`. The most relevant condition types are:
77+
78+
- `Deployed`: the operator created the Kubernetes resources for KAI services.
79+
- `Available`: the KAI service controllers report availability.
80+
- `DependenciesFulfilled`: external dependencies required by the selected configuration are present and healthy.
81+
- `Ready`: the KAI services are available.
82+
83+
When `global.gpuSharingMode` is `NvFractions`, the KAI operator also checks the cluster-scoped `GpuSharingConfig` resource from the GPU-sharing operator, as running fractional GPU pods depends on it.
84+
85+
If all KAI services are deployed and available, but the GPU-sharing operator reports a dependency failure, the KAI `Config` can look like this:
86+
87+
```yaml
88+
apiVersion: kai.scheduler/v1
89+
kind: Config
90+
metadata:
91+
name: kai-config
92+
spec:
93+
global:
94+
gpuSharingMode: NvFractions
95+
status:
96+
conditions:
97+
- type: Deployed
98+
status: "True"
99+
reason: deployed
100+
message: Resources deployed
101+
- type: Available
102+
status: "True"
103+
reason: available
104+
message: System available
105+
- type: DependenciesFulfilled
106+
status: "False"
107+
reason: dependencies_missing
108+
message: Gpu Sharing is not ready. Ready=False, reason=GPUOperatorNotReady, message=ClusterPolicy is not ready
109+
- type: Ready
110+
status: "True"
111+
reason: ready
112+
message: System is ready
113+
```
114+
115+
In this state the KAI pods are healthy, but NvFractions should not be considered fully operational until `DependenciesFulfilled=True`. The dependency message carries the GPU-sharing operator's config-level status reason and message so the failing external dependency is visible directly from the KAI `Config`.
116+
74117
## Logging
75118

76119
By default all KAI services use development-mode logging with colored, human-readable console

pkg/apis/scheduling/v1alpha2/zz_generated.deepcopy.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/operator/controller/config_controller.go

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"errors"
2222

2323
admissionv1 "k8s.io/api/admissionregistration/v1"
24+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2425
"k8s.io/apimachinery/pkg/runtime"
2526
"k8s.io/apimachinery/pkg/types"
2627
vpav1 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
@@ -39,6 +40,7 @@ import (
3940
"github.com/kai-scheduler/KAI-scheduler/pkg/operator/operands/binder"
4041
"github.com/kai-scheduler/KAI-scheduler/pkg/operator/operands/common"
4142
"github.com/kai-scheduler/KAI-scheduler/pkg/operator/operands/deployable"
43+
"github.com/kai-scheduler/KAI-scheduler/pkg/operator/operands/gpu_sharing"
4244
"github.com/kai-scheduler/KAI-scheduler/pkg/operator/operands/known_types"
4345
"github.com/kai-scheduler/KAI-scheduler/pkg/operator/operands/node_scale_adjuster"
4446
"github.com/kai-scheduler/KAI-scheduler/pkg/operator/operands/numa_placement_exporter"
@@ -57,6 +59,7 @@ var ConfigReconcilerOperands = []operands.Operand{
5759
&node_scale_adjuster.NodeScaleAdjuster{},
5860
&admission.Admission{},
5961
&prometheus.Prometheus{},
62+
&gpu_sharing.GpuSharing{},
6063
&scheduler.SchedulerForConfig{},
6164
&numa_placement_exporter.NumaPlacementExporter{},
6265
}
@@ -83,6 +86,7 @@ func (r *ConfigReconciler) SetOperands(ops []operands.Operand) {
8386
// +kubebuilder:rbac:groups="admissionregistration.k8s.io",resources=mutatingwebhookconfigurations;validatingwebhookconfigurations,verbs=get;list;watch;create
8487
// +kubebuilder:rbac:groups="apiextensions.k8s.io",resources=customresourcedefinitions,resourceNames=queues.scheduling.run.ai,verbs=delete;update;patch
8588
// +kubebuilder:rbac:groups="apiextensions.k8s.io",resources=customresourcedefinitions,verbs=get;list;watch;create
89+
// +kubebuilder:rbac:groups="gpu-sharing.kai.scheduler",resources=gpusharingconfigs,verbs=get;list;watch
8690
// +kubebuilder:rbac:groups="nvidia.com",resources=clusterpolicies,verbs=get;list;watch
8791
// +kubebuilder:rbac:groups="monitoring.coreos.com",resources=prometheuses;servicemonitors,verbs=get;list;watch;create;update;patch;delete
8892
// +kubebuilder:rbac:groups="scheduling.run.ai",resources=queues,verbs=get;list;watch
@@ -141,6 +145,7 @@ func (r *ConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res
141145

142146
// SetupWithManager sets up the controller with the Manager.
143147
func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
148+
logger := log.FromContext(context.Background())
144149
for _, collectable := range known_types.KAIConfigRegisteredCollectible {
145150
if err := collectable.InitWithManager(context.Background(), mgr); err != nil {
146151
return err
@@ -159,10 +164,26 @@ func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
159164
builder := ctrl.NewControllerManagedBy(mgr).
160165
For(&kaiv1.Config{})
161166

162-
if checkForClusterPolicy(mgr) {
167+
clusterPolicyExists, err := common.CheckCRDsAvailable(
168+
context.Background(), mgr.GetAPIReader(), "clusterpolicies.nvidia.com",
169+
)
170+
if err != nil {
171+
logger.Info("Failed to check for ClusterPolicy CRD existence", "error", err)
172+
} else if clusterPolicyExists {
163173
builder = builder.Watches(&nvidiav1.ClusterPolicy{}, handler.EnqueueRequestsFromMapFunc(enqueueWatched))
164174
}
165175

176+
gpuSharingConfigExists, err := common.CheckCRDsAvailable(
177+
context.Background(), mgr.GetAPIReader(), gpu_sharing.GpuSharingConfigCRDName,
178+
)
179+
if err != nil {
180+
logger.Info("Failed to check for GpuSharingConfig CRD existence", "error", err)
181+
} else if gpuSharingConfigExists {
182+
gpuSharingConfig := &unstructured.Unstructured{}
183+
gpuSharingConfig.SetGroupVersionKind(gpu_sharing.GpuSharingConfigGVK)
184+
builder = builder.Watches(gpuSharingConfig, handler.EnqueueRequestsFromMapFunc(enqueueWatched))
185+
}
186+
166187
builder = builder.Watches(&kaiv1.SchedulingShard{}, handler.EnqueueRequestsFromMapFunc(enqueueWatched))
167188

168189
for _, collectable := range known_types.KAIConfigRegisteredCollectible {
@@ -181,22 +202,3 @@ func enqueueWatched(_ context.Context, _ client.Object) []ctrl.Request {
181202
},
182203
}
183204
}
184-
185-
func checkForClusterPolicy(mgr ctrl.Manager) bool {
186-
logger := log.FromContext(context.Background())
187-
tempClient, err := client.New(mgr.GetConfig(), client.Options{Scheme: mgr.GetScheme()})
188-
if err != nil {
189-
logger.Info("Failed to create temporary client to check for cluster policy", "error", err)
190-
return false
191-
}
192-
193-
clusterPolicyExists, err := common.CheckCRDsAvailable(
194-
context.Background(), tempClient, "clusterpolicies.nvidia.com",
195-
)
196-
if err != nil {
197-
logger.Info("Failed to check for ClusterPolicy CRD existence", "error", err)
198-
return false
199-
}
200-
201-
return clusterPolicyExists
202-
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2025 NVIDIA CORPORATION
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package gpu_sharing
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
11+
"k8s.io/apimachinery/pkg/api/meta"
12+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13+
"k8s.io/apimachinery/pkg/runtime/schema"
14+
"k8s.io/apimachinery/pkg/types"
15+
"sigs.k8s.io/controller-runtime/pkg/client"
16+
17+
kaiv1 "github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1"
18+
kaiv1common "github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1/common"
19+
)
20+
21+
const GpuSharingConfigCRDName = "gpusharingconfigs.gpu-sharing.kai.scheduler"
22+
23+
var GpuSharingConfigGVK = schema.GroupVersionKind{
24+
Group: "gpu-sharing.kai.scheduler",
25+
Version: "v1alpha1",
26+
Kind: "GpuSharingConfig",
27+
}
28+
29+
type GpuSharing struct{}
30+
31+
func (g *GpuSharing) DesiredState(context.Context, client.Reader, *kaiv1.Config) ([]client.Object, error) {
32+
return nil, nil
33+
}
34+
35+
func (g *GpuSharing) IsDeployed(context.Context, client.Reader) (bool, error) {
36+
return true, nil
37+
}
38+
39+
func (g *GpuSharing) IsAvailable(context.Context, client.Reader) (bool, error) {
40+
return true, nil
41+
}
42+
43+
func (g *GpuSharing) Monitor(context.Context, client.Reader, *kaiv1.Config) error {
44+
return nil
45+
}
46+
47+
func (g *GpuSharing) HasMissingDependencies(
48+
ctx context.Context, readerClient client.Reader, kaiConfig *kaiv1.Config,
49+
) (string, error) {
50+
if !isNvFractionsConfigured(kaiConfig) {
51+
return "", nil
52+
}
53+
54+
gpuSharingConfig := &unstructured.Unstructured{}
55+
gpuSharingConfig.SetGroupVersionKind(GpuSharingConfigGVK)
56+
if err := readerClient.Get(ctx, types.NamespacedName{Name: "default"}, gpuSharingConfig); err != nil {
57+
if apierrors.IsNotFound(err) || meta.IsNoMatchError(err) {
58+
return "Gpu Sharing is not ready. GpuSharingConfig not found", nil
59+
}
60+
return "", err
61+
}
62+
63+
readyConditions, found, err := unstructured.NestedSlice(gpuSharingConfig.Object, "status", "conditions")
64+
if err != nil {
65+
return "", err
66+
}
67+
for _, condition := range readyConditions {
68+
conditionMap, ok := condition.(map[string]interface{})
69+
if !ok {
70+
continue
71+
}
72+
if conditionMap["type"] != "Ready" {
73+
continue
74+
}
75+
if conditionMap["status"] == "True" {
76+
return "", nil
77+
}
78+
return fmt.Sprintf(
79+
"Gpu Sharing is not ready. Ready=%s, reason=%s, message=%s",
80+
conditionFieldString(conditionMap, "status"),
81+
conditionFieldString(conditionMap, "reason"),
82+
conditionFieldString(conditionMap, "message"),
83+
), nil
84+
}
85+
86+
if !found {
87+
return "Gpu Sharing is not ready. GpuSharingConfig conditions not found", nil
88+
}
89+
return "Gpu Sharing is not ready. Ready condition not found", nil
90+
}
91+
92+
func (g *GpuSharing) Name() string {
93+
return "GPU-sharing"
94+
}
95+
96+
func isNvFractionsConfigured(kaiConfig *kaiv1.Config) bool {
97+
return kaiConfig != nil && kaiConfig.Spec.Global != nil && kaiConfig.Spec.Global.GpuSharingMode != nil &&
98+
*kaiConfig.Spec.Global.GpuSharingMode == kaiv1common.GpuSharingModeNvFractions
99+
}
100+
101+
func conditionFieldString(condition map[string]interface{}, field string) string {
102+
value, ok := condition[field].(string)
103+
if !ok {
104+
return ""
105+
}
106+
return value
107+
}

0 commit comments

Comments
 (0)