Skip to content

Commit e99ae45

Browse files
committed
feat(binder): add NvFractions binding plugin
Signed-off-by: davidLif <davidshani12@gmail.com>
1 parent 6b54e5f commit e99ae45

17 files changed

Lines changed: 529 additions & 66 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+
Bind NvFractions GPU memory devices

pkg/apis/kai/v1/binder/binder.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const (
2323
DynamicResourcesPluginName = "dynamicresources"
2424
GPUSharingPluginName = "gpusharing"
2525
HamiCorePluginName = "hamicore"
26+
NvFractionsPluginName = "nvfractions"
2627

2728
BindTimeoutSecondsArgument = "bindTimeoutSeconds"
2829
CDIEnabledArgument = "cdiEnabled"
@@ -35,6 +36,7 @@ var defaultPluginPriorities = map[string]int{
3536
VolumeBindingPluginName: 300,
3637
DynamicResourcesPluginName: 200,
3738
GPUSharingPluginName: 100,
39+
NvFractionsPluginName: 90,
3840
HamiCorePluginName: 50,
3941
}
4042

@@ -139,13 +141,16 @@ func (b *Binder) setDefaultPlugins() {
139141
binderPluginConfig := DefaultPluginsConfig(ptr.Deref(b.VolumeBindingTimeoutSeconds, DefaultBindTimeoutSeconds),
140142
ptr.Deref(b.CDIEnabled, DefaultCDIEnabled))
141143

142-
// When CDIEnabled is unset at the API level, leave the gpusharing cdiEnabled
143-
// argument unbaked so the operator can resolve it (auto-detect) without
144-
// having to distinguish a defaulted value from a user-supplied one.
144+
// When CDIEnabled is unset at the API level, leave the cdiEnabled argument
145+
// unbaked on the CDI-aware plugins (gpusharing, nvfractions) so the operator
146+
// can resolve it (auto-detect) without having to distinguish a defaulted value
147+
// from a user-supplied one.
145148
if b.CDIEnabled == nil {
146-
gpuSharingDefault := binderPluginConfig[GPUSharingPluginName]
147-
delete(gpuSharingDefault.Arguments, CDIEnabledArgument)
148-
binderPluginConfig[GPUSharingPluginName] = gpuSharingDefault
149+
for _, name := range []string{GPUSharingPluginName, NvFractionsPluginName} {
150+
pluginDefault := binderPluginConfig[name]
151+
delete(pluginDefault.Arguments, CDIEnabledArgument)
152+
binderPluginConfig[name] = pluginDefault
153+
}
149154
}
150155

151156
for name, userBinderConfig := range b.Plugins {
@@ -198,6 +203,13 @@ func DefaultPluginsConfig(bindTimeoutSeconds int, cdiEnabled bool) map[string]Pl
198203
CDIEnabledArgument: strconv.FormatBool(cdiEnabled),
199204
},
200205
},
206+
NvFractionsPluginName: {
207+
Enabled: ptr.To(false),
208+
Priority: ptr.To(defaultPluginPriorities[NvFractionsPluginName]),
209+
Arguments: map[string]string{
210+
CDIEnabledArgument: strconv.FormatBool(cdiEnabled),
211+
},
212+
},
201213
HamiCorePluginName: {
202214
Enabled: ptr.To(false),
203215
Priority: ptr.To(defaultPluginPriorities[HamiCorePluginName]),

pkg/apis/kai/v1/binder/binder_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ var _ = Describe("Binder", func() {
3232
Expect(binder.Service.Resources.Requests[v1.ResourceMemory]).To(Equal(resource.MustParse("200Mi")))
3333
Expect(binder.Service.Resources.Limits[v1.ResourceCPU]).To(Equal(resource.MustParse("100m")))
3434
Expect(binder.Service.Resources.Limits[v1.ResourceMemory]).To(Equal(resource.MustParse("200Mi")))
35-
Expect(binder.Plugins).To(HaveLen(4))
35+
Expect(binder.Plugins).To(HaveLen(5))
3636
Expect(*binder.Plugins[VolumeBindingPluginName].Priority).To(Equal(defaultPluginPriorities[VolumeBindingPluginName]))
3737
Expect(*binder.Plugins[DynamicResourcesPluginName].Priority).To(Equal(defaultPluginPriorities[DynamicResourcesPluginName]))
3838
Expect(*binder.Plugins[GPUSharingPluginName].Priority).To(Equal(defaultPluginPriorities[GPUSharingPluginName]))
39+
Expect(*binder.Plugins[NvFractionsPluginName].Priority).To(Equal(defaultPluginPriorities[NvFractionsPluginName]))
3940
Expect(binder.Plugins[VolumeBindingPluginName].Arguments[BindTimeoutSecondsArgument]).
4041
To(Equal(strconv.Itoa(DefaultBindTimeoutSeconds)))
4142
Expect(binder.Plugins[DynamicResourcesPluginName].Arguments[BindTimeoutSecondsArgument]).

pkg/binder/binding/binder.go

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,25 @@ func (b *Binder) Bind(ctx context.Context, pod *v1.Pod, node *v1.Node, bindReque
5454
}
5555
}
5656
bindingState := &state.BindingState{
57+
BindingPodAnnotations: map[string]string{
58+
constants.ReceivedResourceType: bindRequest.Spec.ReceivedResourceType,
59+
},
5760
ReservedGPUIds: reservedGPUIds,
5861
}
62+
if len(bindRequest.Spec.PredictedNUMAZones) > 0 {
63+
placement, err := json.Marshal(bindRequest.Spec.PredictedNUMAZones)
64+
if err != nil {
65+
return err
66+
}
67+
bindingState.BindingPodAnnotations[constants.NumaPlacementPredicted] = string(placement)
68+
}
5969

6070
err = b.plugins.PreBind(ctx, pod, node, bindRequest, bindingState)
6171
if err != nil {
6272
return err
6373
}
6474

65-
err = b.patchBindAnnotations(ctx, pod, bindRequest)
75+
err = b.patchPodBindingAnnotations(ctx, pod, bindingState)
6676
if err != nil {
6777
return fmt.Errorf("failed to patch pod <%s/%s> with bind annotations: %w", pod.Namespace, pod.Name, err)
6878
}
@@ -127,22 +137,10 @@ func (b *Binder) reserveGPUs(ctx context.Context, pod *v1.Pod, bindRequest *v1al
127137
return gpuIndexes, nil
128138
}
129139

130-
func (b *Binder) patchBindAnnotations(ctx context.Context, pod *v1.Pod, bindRequest *v1alpha2.BindRequest) error {
131-
annotations := map[string]string{
132-
constants.ReceivedResourceType: bindRequest.Spec.ReceivedResourceType,
133-
}
134-
135-
if len(bindRequest.Spec.PredictedNUMAZones) > 0 {
136-
placement, err := json.Marshal(bindRequest.Spec.PredictedNUMAZones)
137-
if err != nil {
138-
return err
139-
}
140-
annotations[constants.NumaPlacementPredicted] = string(placement)
141-
}
142-
140+
func (b *Binder) patchPodBindingAnnotations(ctx context.Context, pod *v1.Pod, bindingState *state.BindingState) error {
143141
patchBytes, err := json.Marshal(map[string]interface{}{
144142
"metadata": map[string]interface{}{
145-
"annotations": annotations,
143+
"annotations": bindingState.BindingPodAnnotations,
146144
},
147145
})
148146
if err != nil {

pkg/binder/binding/default_binder_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ func TestBindApplyResourceReceivedType(t *testing.T) {
182182

183183
err := binder.Bind(context.TODO(), pod, &v1.Node{ObjectMeta: metav1.ObjectMeta{
184184
Name: "my-node",
185+
Labels: map[string]string{
186+
constants.NvidiaGpuMemory: "1000",
187+
},
185188
}}, bindRequest)
186189

187190
assert.Nil(t, err)

pkg/binder/binding/fraction_binder_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ var happyFlowObjectsBc = []runtime.Object{
7373
&v1.Node{
7474
ObjectMeta: metav1.ObjectMeta{
7575
Name: "my-node",
76+
Labels: map[string]string{
77+
constants.NvidiaGpuMemory: "1000",
78+
},
7679
},
7780
},
7881
}
@@ -117,6 +120,9 @@ var happyFlowObjects = []runtime.Object{
117120
},
118121
ObjectMeta: metav1.ObjectMeta{
119122
Name: "my-node",
123+
Labels: map[string]string{
124+
constants.NvidiaGpuMemory: "1000",
125+
},
120126
},
121127
},
122128
}
@@ -198,6 +204,9 @@ var _ = Describe("FractionBinder", func() {
198204
result := testedBinder.Bind(
199205
context.TODO(), pod, &v1.Node{ObjectMeta: metav1.ObjectMeta{
200206
Name: "my-node",
207+
Labels: map[string]string{
208+
constants.NvidiaGpuMemory: "1000",
209+
},
201210
}}, bindRequest)
202211
if testData.expectedErrorContains != "" {
203212
Expect(result).NotTo(BeNil())
@@ -260,6 +269,9 @@ var _ = Describe("FractionBinder", func() {
260269
result := testedBinder.Bind(
261270
context.TODO(), pod, &v1.Node{ObjectMeta: metav1.ObjectMeta{
262271
Name: "my-node",
272+
Labels: map[string]string{
273+
constants.NvidiaGpuMemory: "1000",
274+
},
263275
}}, bindRequest)
264276

265277
Expect(result).NotTo(BeNil())

pkg/binder/plugins/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const (
1717
DynamicResourcesPluginName = kaiv1binder.DynamicResourcesPluginName
1818
GPUSharingPluginName = kaiv1binder.GPUSharingPluginName
1919
HamiCorePluginName = kaiv1binder.HamiCorePluginName
20+
NvFractionsPluginName = kaiv1binder.NvFractionsPluginName
2021

2122
BindTimeoutSecondsArgument = kaiv1binder.BindTimeoutSecondsArgument
2223
CDIEnabledArgument = kaiv1binder.CDIEnabledArgument

pkg/binder/plugins/factory.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/kai-scheduler/KAI-scheduler/pkg/binder/plugins/gpusharing"
1717
"github.com/kai-scheduler/KAI-scheduler/pkg/binder/plugins/hamicore"
1818
k8splugins "github.com/kai-scheduler/KAI-scheduler/pkg/binder/plugins/k8s-plugins"
19+
"github.com/kai-scheduler/KAI-scheduler/pkg/binder/plugins/nvfractions"
1920
)
2021

2122
type PluginBuildContext struct {
@@ -51,6 +52,7 @@ func InitDefaultPlugins() {
5152
RegisterPluginBuilder(DynamicResourcesPluginName, newDynamicResourcesPlugin)
5253
RegisterPluginBuilder(GPUSharingPluginName, newGPUSharingPlugin)
5354
RegisterPluginBuilder(HamiCorePluginName, newHamiCorePlugin)
55+
RegisterPluginBuilder(NvFractionsPluginName, newNvFractionsPlugin)
5456
}
5557

5658
func BuildConfiguredPlugins(buildContext PluginBuildContext, config Config) (*BinderPlugins, error) {
@@ -112,6 +114,14 @@ func newHamiCorePlugin(buildContext PluginBuildContext, _ map[string]string) (Pl
112114
return hamicore.New(buildContext.KubeClient), nil
113115
}
114116

117+
func newNvFractionsPlugin(_ PluginBuildContext, arguments map[string]string) (Plugin, error) {
118+
cdiEnabled, err := boolArgument(arguments, CDIEnabledArgument)
119+
if err != nil {
120+
return nil, err
121+
}
122+
return nvfractions.New(cdiEnabled), nil
123+
}
124+
115125
func validateDependentPlugins(config Config) error {
116126
hamiCoreCfg, hamiCoreFound := config[HamiCorePluginName]
117127
if !hamiCoreFound || (hamiCoreCfg.Enabled != nil && !*hamiCoreCfg.Enabled) {

pkg/binder/plugins/gpusharing/gpu_sharing.go

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"errors"
99
"fmt"
10+
"strconv"
1011
"strings"
1112

1213
"golang.org/x/exp/slices"
@@ -15,10 +16,11 @@ import (
1516
"sigs.k8s.io/controller-runtime/pkg/log"
1617

1718
"github.com/kai-scheduler/KAI-scheduler/pkg/apis/scheduling/v1alpha2"
18-
"github.com/kai-scheduler/KAI-scheduler/pkg/binder/common/gpusharingconfigmap"
19-
2019
"github.com/kai-scheduler/KAI-scheduler/pkg/binder/common"
20+
"github.com/kai-scheduler/KAI-scheduler/pkg/binder/common/gpusharingconfigmap"
2121
"github.com/kai-scheduler/KAI-scheduler/pkg/binder/plugins/state"
22+
"github.com/kai-scheduler/KAI-scheduler/pkg/common/constants"
23+
"github.com/kai-scheduler/KAI-scheduler/pkg/common/resources"
2224
)
2325

2426
const (
@@ -42,7 +44,7 @@ func (p *GPUSharing) Name() string {
4244
}
4345

4446
func (p *GPUSharing) PreBind(
45-
ctx context.Context, pod *v1.Pod, _ *v1.Node, bindRequest *v1alpha2.BindRequest, state *state.BindingState,
47+
ctx context.Context, pod *v1.Pod, node *v1.Node, bindRequest *v1alpha2.BindRequest, state *state.BindingState,
4648
) error {
4749
if !common.IsSharedGPUAllocation(bindRequest) {
4850
return nil
@@ -60,6 +62,11 @@ func (p *GPUSharing) PreBind(
6062
return fmt.Errorf("failed to get fraction container ref: %w", err)
6163
}
6264

65+
err = addNvFractionsAnnotationIfMissing(pod, node, bindRequest, containerRef, state)
66+
if err != nil {
67+
return fmt.Errorf("failed to add NvFractions annotation: %w", err)
68+
}
69+
6370
err = p.createCapabilitiesConfigMapIfMissing(ctx, pod, containerRef)
6471
if err != nil {
6572
return fmt.Errorf("failed to create capabilities configmap: %w", err)
@@ -79,6 +86,44 @@ func (p *GPUSharing) PreBind(
7986
return common.SetGPUPortion(ctx, p.kubeClient, pod, containerRef, bindRequest.Spec.ReceivedGPU.Portion)
8087
}
8188

89+
func addNvFractionsAnnotationIfMissing(pod *v1.Pod, node *v1.Node, bindRequest *v1alpha2.BindRequest,
90+
containerRef *gpusharingconfigmap.PodContainerRef, bindingState *state.BindingState) error {
91+
annotationKey := resources.CalcGpuFractionAnnotationForContainer(containerRef.Container.Name)
92+
if _, found := pod.Annotations[annotationKey]; found {
93+
return nil
94+
}
95+
96+
if node == nil || bindRequest == nil || bindRequest.Spec.ReceivedGPU == nil {
97+
return fmt.Errorf("missing data for NvFractions annotation calculation")
98+
}
99+
100+
gpuMemoryStr, foundGPUMemory := node.Labels[constants.NvidiaGpuMemory]
101+
if !foundGPUMemory {
102+
return fmt.Errorf("node does not include %s label - failed to add NvFractions annotation", constants.NvidiaGpuMemory)
103+
}
104+
105+
totalGPUMemoryMiB, err := strconv.ParseFloat(gpuMemoryStr, 64)
106+
if err != nil || totalGPUMemoryMiB <= 0 {
107+
return fmt.Errorf("invalid %s label value %q - failed to add NvFractions annotation", constants.NvidiaGpuMemory, gpuMemoryStr)
108+
}
109+
110+
gpuPortion, err := strconv.ParseFloat(bindRequest.Spec.ReceivedGPU.Portion, 64)
111+
if err != nil || gpuPortion <= 0 {
112+
return fmt.Errorf("invalid received gpu portion %q - failed to add NvFractions annotation", bindRequest.Spec.ReceivedGPU.Portion)
113+
}
114+
115+
gpuMemory := uint64(totalGPUMemoryMiB * gpuPortion)
116+
if gpuMemory == 0 {
117+
return fmt.Errorf("calculated gpu memory request is zero")
118+
}
119+
120+
if bindingState.BindingPodAnnotations == nil {
121+
bindingState.BindingPodAnnotations = map[string]string{}
122+
}
123+
bindingState.BindingPodAnnotations[annotationKey] = resources.GpuMemoryAnnotationToNvFractionsMemoryRequest(gpuMemory).String()
124+
return nil
125+
}
126+
82127
func (p *GPUSharing) createCapabilitiesConfigMapIfMissing(ctx context.Context, pod *v1.Pod,
83128
containerRef *gpusharingconfigmap.PodContainerRef) error {
84129
capabilitiesConfigMapName, err := gpusharingconfigmap.ExtractCapabilitiesConfigMapName(pod, containerRef)

0 commit comments

Comments
 (0)