Skip to content

Commit e4aea15

Browse files
committed
Update the compute mode annotation according to the updated API with gpu-sharing operator and add validations
1 parent f17e5c5 commit e4aea15

18 files changed

Lines changed: 298 additions & 36 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Changed
2+
body: |-
3+
Renamed gpu-compute-sharing-mode annotation to per-container nvidia.com/container.<name>.gpu-compute.mode with validation

docs/gpu-sharing/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,13 +314,13 @@ kubectl apply -f gpu-sharing-non-default-container.yaml
314314

315315
### Compute sharing mode within a fractional GPU group
316316

317-
Pods that share a GPU must use compatible compute sharing behavior within the same fractional GPU group. Use `kai.scheduler/gpu-compute-sharing-mode` when the workload needs to declare its compute sharing mode explicitly:
317+
Pods that share a GPU must use compatible compute sharing behavior within the same fractional GPU group. Use `nvidia.com/container.<container-name>.gpu-compute.mode` when the workload needs to declare its compute sharing mode explicitly. The container name must match the container targeted by the pod's other fractional GPU annotations:
318318

319319
```yaml
320320
metadata:
321321
annotations:
322322
gpu-fraction: "0.5"
323-
kai.scheduler/gpu-compute-sharing-mode: time-slicing
323+
nvidia.com/container.gpu-workload.gpu-compute.mode: time-slicing
324324
```
325325

326326
KAI keeps pods with incompatible compute sharing modes out of the same fractional GPU group.

docs/gpu-sharing/nv-fraction/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,18 @@ Setting a `limit` is useful for pods that have occasional "bursts" of gpu memory
6666
## Choose a compute-sharing mode
6767

6868
GPU memory and GPU compute mode are selected independently. The
69-
`kai.scheduler/gpu-compute-sharing-mode` annotation chooses how workloads in
70-
the same fractional GPU group share compute. KAI supports these values:
69+
`nvidia.com/container.<container-name>.gpu-compute.mode` annotation chooses how
70+
workloads in the same fractional GPU group share compute. KAI supports these
71+
values:
7172

7273
| Mode | How workloads run | Best fit |
7374
| --- | --- | --- |
7475
| `time-slicing` | Workloads take turns using the GPU. Unused time is available to other workloads. This is the default. | Bursty development, notebooks, and throughput-oriented or latency-tolerant inference. |
7576
| `sm-sharing` | Workloads run concurrently and share the GPU streaming multiprocessors (SMs). | Steady or latency-sensitive inference, and coordinated multi-GPU or multi-Pod workloads. Requires MPS. |
7677

77-
Set `kai.scheduler/gpu-compute-sharing-mode` in the NvFractions Pod manifest.
78-
The [GPU Sharing guide](../README.md#compute-sharing-mode-within-a-fractional-gpu-group)
78+
Set `nvidia.com/container.<container-name>.gpu-compute.mode` in the NvFractions
79+
Pod manifest, using the same container name as the pod's other NvFractions
80+
annotations. The [GPU Sharing guide](../README.md#compute-sharing-mode-within-a-fractional-gpu-group)
7981
shows the annotation in context.
8082

8183
![Time-slicing and SM-sharing](compute-mode.png)

pkg/binder/binding/resourcereservation/resource_reservation.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ func (rsc *service) waitForGPUReservationPodAllocation(
578578

579579
func (rsc *service) createResourceReservationPod(
580580
nodeName string, fractionalGpuGroup schedulingv1alpha2.FractionalGpuGroup,
581-
podName string, resources v1.ResourceRequirements,
581+
podName string, containerResources v1.ResourceRequirements,
582582
) (*v1.Pod, error) {
583583
fractionalGpuGroup = fractionalGpuGroup.WithDefaults()
584584
podSpec := &v1.Pod{
@@ -590,8 +590,8 @@ func (rsc *service) createResourceReservationPod(
590590
constants.GPUGroup: fractionalGpuGroup.ID,
591591
},
592592
Annotations: map[string]string{
593-
karpenterv1.DoNotDisruptAnnotationKey: "true",
594-
constants.GpuComputeSharingMode: string(fractionalGpuGroup.ComputeSharingMode),
593+
karpenterv1.DoNotDisruptAnnotationKey: "true",
594+
resources.CalcGpuComputeSharingModeAnnotationForContainer(resourceReservation): string(fractionalGpuGroup.ComputeSharingMode),
595595
},
596596
},
597597
Spec: v1.PodSpec{
@@ -609,7 +609,7 @@ func (rsc *service) createResourceReservationPod(
609609
Name: resourceReservation,
610610
Image: rsc.reservationPodImage,
611611
ImagePullPolicy: v1.PullIfNotPresent,
612-
Resources: resources,
612+
Resources: containerResources,
613613
SecurityContext: rsc.reservationContainerSecurityContext,
614614
Env: []v1.EnvVar{
615615
{

pkg/binder/binding/resourcereservation/resource_reservation_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
schedulingv1alpha2 "github.com/kai-scheduler/KAI-scheduler/pkg/apis/scheduling/v1alpha2"
2828
"github.com/kai-scheduler/KAI-scheduler/pkg/common/constants"
29+
"github.com/kai-scheduler/KAI-scheduler/pkg/common/resources"
2930
)
3031

3132
const (
@@ -905,7 +906,7 @@ var _ = Describe("ResourceReservationService", func() {
905906
kubeClient: fake.NewClientBuilder().WithScheme(testScheme).Build(),
906907
}
907908

908-
resources := v1.ResourceRequirements{
909+
containerResources := v1.ResourceRequirements{
909910
Limits: v1.ResourceList{
910911
"nvidia.com/gpu": resource.MustParse("1"),
911912
},
@@ -918,7 +919,7 @@ var _ = Describe("ResourceReservationService", func() {
918919
pod, err := rsc.createResourceReservationPod(nodeName, schedulingv1alpha2.FractionalGpuGroup{
919920
ID: gpuGroup,
920921
ComputeSharingMode: schedulingv1alpha2.GPUComputeSharingModeTimeSlicing,
921-
}, podName, resources)
922+
}, podName, containerResources)
922923
Expect(err).To(BeNil())
923924
Expect(pod).NotTo(BeNil())
924925

@@ -927,7 +928,7 @@ var _ = Describe("ResourceReservationService", func() {
927928
Expect(pod.Namespace).To(Equal("kai-resource-reservation"))
928929
Expect(pod.Labels[constants.AppLabelName]).To(Equal("kai-reservation"))
929930
Expect(pod.Labels[constants.GPUGroup]).To(Equal(gpuGroup))
930-
Expect(pod.Annotations[constants.GpuComputeSharingMode]).To(Equal(
931+
Expect(pod.Annotations[resources.CalcGpuComputeSharingModeAnnotationForContainer("resource-reservation")]).To(Equal(
931932
string(schedulingv1alpha2.GPUComputeSharingModeTimeSlicing)))
932933

933934
// PodSpec checks
@@ -941,7 +942,7 @@ var _ = Describe("ResourceReservationService", func() {
941942
Expect(container.Name).To(Equal("resource-reservation"))
942943
Expect(container.Image).To(Equal("nvidia/kai-reservation:latest"))
943944
Expect(container.ImagePullPolicy).To(Equal(v1.PullIfNotPresent))
944-
Expect(container.Resources).To(Equal(resources))
945+
Expect(container.Resources).To(Equal(containerResources))
945946

946947
// Check env vars
947948
podNameEnv := v1.EnvVar{
@@ -983,7 +984,7 @@ var _ = Describe("ResourceReservationService", func() {
983984
v1.ResourceRequirements{},
984985
)
985986
Expect(err).To(BeNil())
986-
Expect(pod.Annotations[constants.GpuComputeSharingMode]).To(Equal(
987+
Expect(pod.Annotations[resources.CalcGpuComputeSharingModeAnnotationForContainer("resource-reservation")]).To(Equal(
987988
string(schedulingv1alpha2.GPUComputeSharingModeSMSharing)))
988989
})
989990
})

pkg/common/constants/constants.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ const (
5656
SkipPodGrouperAnnotation = "kai.scheduler/skip-podgrouper"
5757
GpuFraction = "gpu-fraction"
5858
GpuFractionContainerName = "gpu-fraction-container-name"
59-
GpuComputeSharingMode = "kai.scheduler/gpu-compute-sharing-mode"
6059
GpuMemory = "gpu-memory"
6160
ReceivedResourceType = "received-resource-type"
6261
GpuFractionsNumDevices = "gpu-fraction-num-devices"
@@ -84,6 +83,7 @@ const (
8483
NvFractionsMemoryRequestSuffix = ".gpu-memory.request"
8584
NvFractionsMemoryLimitSuffix = ".gpu-memory.limit"
8685
NvFractionsVisibleDevicesSuffix = ".gpus.devices"
86+
GpuComputeSharingModeSuffix = ".gpu-compute.mode"
8787

8888
KaiFractionContainerAnnotationPrefix = "kai.scheduler/container."
8989
GpuMemoryPortionLimitSuffix = ".gpu-memory.portion.limit"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2025 NVIDIA CORPORATION
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package resources
5+
6+
import (
7+
"strings"
8+
9+
v1 "k8s.io/api/core/v1"
10+
11+
"github.com/kai-scheduler/KAI-scheduler/pkg/common/constants"
12+
)
13+
14+
// CalcGpuComputeSharingModeAnnotationForContainer returns the per-container
15+
// GPU compute-sharing-mode annotation key for containerName.
16+
func CalcGpuComputeSharingModeAnnotationForContainer(containerName string) string {
17+
return constants.NvFractionsAnnotationPrefix + containerName + constants.GpuComputeSharingModeSuffix
18+
}
19+
20+
// ExtractGpuComputeSharingModeAnnotation returns the container name and raw
21+
// value of the pod's gpu-compute.mode annotation, if present.
22+
func ExtractGpuComputeSharingModeAnnotation(pod *v1.Pod) (containerName string, rawValue string, found bool) {
23+
for annotationKey, annotationValue := range pod.Annotations {
24+
if !isGpuComputeSharingModeAnnotation(annotationKey) {
25+
continue
26+
}
27+
return gpuComputeSharingModeContainerName(annotationKey), annotationValue, true
28+
}
29+
return "", "", false
30+
}
31+
32+
func isGpuComputeSharingModeAnnotation(annotationKey string) bool {
33+
return strings.HasPrefix(annotationKey, constants.NvFractionsAnnotationPrefix) &&
34+
strings.HasSuffix(annotationKey, constants.GpuComputeSharingModeSuffix)
35+
}
36+
37+
func gpuComputeSharingModeContainerName(annotationKey string) string {
38+
containerName := strings.TrimPrefix(annotationKey, constants.NvFractionsAnnotationPrefix)
39+
return strings.TrimSuffix(containerName, constants.GpuComputeSharingModeSuffix)
40+
}

pkg/common/resources/gpu_sharing.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
v1 "k8s.io/api/core/v1"
1313
"k8s.io/apimachinery/pkg/api/resource"
1414

15+
schedulingv1alpha2 "github.com/kai-scheduler/KAI-scheduler/pkg/apis/scheduling/v1alpha2"
1516
"github.com/kai-scheduler/KAI-scheduler/pkg/common/constants"
1617
)
1718

@@ -44,8 +45,9 @@ type PodGPUFractionRequest struct {
4445
}
4546

4647
type NvFractionsContainerRequest struct {
47-
Request *resource.Quantity
48-
Limit *resource.Quantity
48+
Request *resource.Quantity
49+
Limit *resource.Quantity
50+
ComputeMode *schedulingv1alpha2.GPUComputeSharingMode
4951
}
5052

5153
var (
@@ -243,3 +245,8 @@ func GpuMemoryAnnotationToNvFractionsMemoryRequest(gpuMemory uint64) *resource.Q
243245
memory := resource.MustParse(fmt.Sprintf("%dMi", gpuMemory))
244246
return &memory
245247
}
248+
249+
func IsValidGPUComputeSharingMode(mode string) bool {
250+
return mode == string(schedulingv1alpha2.GPUComputeSharingModeTimeSlicing) ||
251+
mode == string(schedulingv1alpha2.GPUComputeSharingModeSMSharing)
252+
}

pkg/common/resources/gpu_sharing_nvfractions.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
v1 "k8s.io/api/core/v1"
1111
"k8s.io/apimachinery/pkg/api/resource"
1212

13+
schedulingv1alpha2 "github.com/kai-scheduler/KAI-scheduler/pkg/apis/scheduling/v1alpha2"
1314
"github.com/kai-scheduler/KAI-scheduler/pkg/common/constants"
1415
)
1516

@@ -18,6 +19,7 @@ type nvFractionsAnnotationType int
1819
const (
1920
nvFractionsRequestAnnotation nvFractionsAnnotationType = iota
2021
nvFractionsLimitAnnotation
22+
nvFractionsComputeModeAnnotation
2123
)
2224

2325
func CalcGpuFractionAnnotationForContainer(containerName string) string {
@@ -38,18 +40,32 @@ func ExtractNvFractionsData(pod *v1.Pod) (map[string]NvFractionsContainerRequest
3840
if !strings.HasPrefix(annotationKey, constants.NvFractionsAnnotationPrefix) {
3941
continue
4042
}
43+
if isNvFractionsDeviceListAnnotation(annotationKey) {
44+
continue
45+
}
4146

4247
containerName, annotationType, err := parseNvFractionsAnnotationKey(annotationKey)
4348
if err != nil {
4449
return nil, err
4550
}
4651

52+
containerData := fractionsData[containerName]
53+
54+
if annotationType == nvFractionsComputeModeAnnotation {
55+
if !IsValidGPUComputeSharingMode(annotationValue) {
56+
return nil, fmt.Errorf("invalid NvFractions compute mode: %s", annotationValue)
57+
}
58+
mode := schedulingv1alpha2.GPUComputeSharingMode(annotationValue)
59+
containerData.ComputeMode = &mode
60+
fractionsData[containerName] = containerData
61+
continue
62+
}
63+
4764
gpuMemory, err := parseNvFractionsAnnotationValue(annotationKey, annotationValue)
4865
if err != nil {
4966
return nil, err
5067
}
5168

52-
containerData := fractionsData[containerName]
5369
if annotationType == nvFractionsRequestAnnotation {
5470
containerData.Request = &gpuMemory
5571
} else {
@@ -75,6 +91,15 @@ func getNvFractionData(pod *v1.Pod) (*NvFractionsContainerRequest, error) {
7591
return nil, nil
7692
}
7793

94+
// isNvFractionsDeviceListAnnotation reports whether annotationKey is the
95+
// device-list annotation. It shares the NvFractions prefix but, unlike
96+
// request/limit/compute-mode, isn't part of the customer's fractional GPU
97+
// request - it's written by the binder after scheduling - so it must be
98+
// skipped here rather than treated as an invalid key.
99+
func isNvFractionsDeviceListAnnotation(annotationKey string) bool {
100+
return strings.HasSuffix(annotationKey, constants.NvFractionsVisibleDevicesSuffix)
101+
}
102+
78103
func parseNvFractionsAnnotationKey(annotationKey string) (string, nvFractionsAnnotationType, error) {
79104
containerNameWithSuffix := strings.TrimPrefix(annotationKey, constants.NvFractionsAnnotationPrefix)
80105
if strings.HasSuffix(annotationKey, constants.NvFractionsMemoryRequestSuffix) {
@@ -85,6 +110,10 @@ func parseNvFractionsAnnotationKey(annotationKey string) (string, nvFractionsAnn
85110
containerName := strings.TrimSuffix(containerNameWithSuffix, constants.NvFractionsMemoryLimitSuffix)
86111
return containerName, nvFractionsLimitAnnotation, nil
87112
}
113+
if strings.HasSuffix(annotationKey, constants.GpuComputeSharingModeSuffix) {
114+
containerName := strings.TrimSuffix(containerNameWithSuffix, constants.GpuComputeSharingModeSuffix)
115+
return containerName, nvFractionsComputeModeAnnotation, nil
116+
}
88117
return "", 0, fmt.Errorf("invalid NvFractions annotation key: %s", annotationKey)
89118
}
90119

pkg/common/resources/gpu_sharing_nvfractions_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"testing"
88

99
"k8s.io/apimachinery/pkg/api/resource"
10+
"k8s.io/utils/ptr"
1011

12+
schedulingv1alpha2 "github.com/kai-scheduler/KAI-scheduler/pkg/apis/scheduling/v1alpha2"
1113
"github.com/kai-scheduler/KAI-scheduler/pkg/common/constants"
1214
)
1315

@@ -75,6 +77,31 @@ func TestExtractNvFractionsData(t *testing.T) {
7577
},
7678
},
7779
},
80+
{
81+
name: "compute sharing mode annotation does not break extraction of a sibling container's request",
82+
annotations: map[string]string{
83+
constants.NvFractionsAnnotationPrefix + "main" + constants.NvFractionsMemoryRequestSuffix: "1Gi",
84+
CalcGpuComputeSharingModeAnnotationForContainer("main"): "sm-sharing",
85+
},
86+
want: map[string]NvFractionsContainerRequest{
87+
"main": {
88+
Request: quantityPtr("1Gi"),
89+
ComputeMode: ptr.To(schedulingv1alpha2.GPUComputeSharingModeSMSharing),
90+
},
91+
},
92+
},
93+
{
94+
name: "skips device list annotation",
95+
annotations: map[string]string{
96+
constants.NvFractionsAnnotationPrefix + "main" + constants.NvFractionsMemoryRequestSuffix: "1Gi",
97+
CalcGpuVisibleDevicesAnnotationForContainer("main"): "gpu-0",
98+
},
99+
want: map[string]NvFractionsContainerRequest{
100+
"main": {
101+
Request: quantityPtr("1Gi"),
102+
},
103+
},
104+
},
78105
{
79106
name: "rejects invalid annotation key",
80107
annotations: map[string]string{
@@ -130,6 +157,12 @@ func TestParseNvFractionsAnnotationKey(t *testing.T) {
130157
wantContainerName: "main",
131158
wantType: nvFractionsLimitAnnotation,
132159
},
160+
{
161+
name: "compute sharing mode annotation",
162+
annotationKey: constants.NvFractionsAnnotationPrefix + "main" + constants.GpuComputeSharingModeSuffix,
163+
wantContainerName: "main",
164+
wantType: nvFractionsComputeModeAnnotation,
165+
},
133166
{
134167
name: "invalid annotation",
135168
annotationKey: constants.NvFractionsAnnotationPrefix + "main",

0 commit comments

Comments
 (0)