Skip to content

Commit aa7a91b

Browse files
Merge pull request llm-d-incubation#198 from osswangxining/lpp-ctl-common
Add common logic for lpp controller
2 parents 7a838e2 + 159bf7b commit aa7a91b

File tree

3 files changed

+225
-0
lines changed

3 files changed

+225
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2025 The llm-d Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package launcherpool
18+
19+
import (
20+
"fmt"
21+
)
22+
23+
// NodeLauncherKey defines the unique identifier for a (Node, LauncherConfig) pair
24+
type NodeLauncherKey struct {
25+
LauncherConfigName string
26+
NodeName string
27+
}
28+
29+
func (k NodeLauncherKey) String() string {
30+
return fmt.Sprintf("%s/%s", k.LauncherConfigName, k.NodeName)
31+
}
32+
33+
// MapToLoggable converts a map of NodeLauncherKey to int32 values into a string representation.
34+
// This function formats the map as a string with the format "{namespace/name/node:count, ...}"
35+
// for debugging and logging purposes.
36+
func MapToLoggable[Key interface {
37+
comparable
38+
fmt.Stringer
39+
}, Val any](m map[Key]Val) map[string]any {
40+
result := make(map[string]any, len(m))
41+
for k, v := range m {
42+
result[k.String()] = v
43+
}
44+
return result
45+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
Copyright 2025 The llm-d Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package launcherpool
18+
19+
import (
20+
fmav1alpha1 "github.com/llm-d-incubation/llm-d-fast-model-actuation/api/fma/v1alpha1"
21+
corev1 "k8s.io/api/core/v1"
22+
)
23+
24+
// matchesResourceConditions checks if a node's allocatable resources match the specified ranges
25+
// If no resource ranges are specified, match all nodes
26+
func matchesResourceConditions(allocatable corev1.ResourceList, ranges fmav1alpha1.ResourceRanges) bool {
27+
for resourceName, rr := range ranges {
28+
// Get the resource quantity from node's allocatable resources
29+
qty, exists := allocatable[resourceName]
30+
if !exists {
31+
// If the node doesn't have this resource, it doesn't match
32+
return false
33+
}
34+
// Check minimum requirement
35+
if rr.Min != nil && qty.Cmp(*rr.Min) < 0 {
36+
return false
37+
}
38+
// Check maximum requirement
39+
if rr.Max != nil && qty.Cmp(*rr.Max) > 0 {
40+
return false
41+
}
42+
}
43+
return true
44+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package launcherpool
2+
3+
import (
4+
"testing"
5+
6+
fmav1alpha1 "github.com/llm-d-incubation/llm-d-fast-model-actuation/api/fma/v1alpha1"
7+
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/apimachinery/pkg/api/resource"
9+
)
10+
11+
func TestMatchesResourceConditions(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
allocatable corev1.ResourceList
15+
ranges fmav1alpha1.ResourceRanges
16+
expected bool
17+
}{
18+
{
19+
name: "empty ranges should match all nodes",
20+
allocatable: corev1.ResourceList{
21+
corev1.ResourceCPU: resource.MustParse("2"),
22+
corev1.ResourceMemory: resource.MustParse("4Gi"),
23+
},
24+
ranges: fmav1alpha1.ResourceRanges{},
25+
expected: true,
26+
},
27+
{
28+
name: "node missing required resource should not match",
29+
allocatable: corev1.ResourceList{
30+
corev1.ResourceCPU: resource.MustParse("2"),
31+
},
32+
ranges: fmav1alpha1.ResourceRanges{
33+
corev1.ResourceMemory: fmav1alpha1.ResourceRange{
34+
Min: resourcePtr(resource.MustParse("2Gi")),
35+
},
36+
},
37+
expected: false,
38+
},
39+
{
40+
name: "resource below minimum should not match",
41+
allocatable: corev1.ResourceList{
42+
corev1.ResourceCPU: resource.MustParse("1"),
43+
},
44+
ranges: fmav1alpha1.ResourceRanges{
45+
corev1.ResourceCPU: fmav1alpha1.ResourceRange{
46+
Min: resourcePtr(resource.MustParse("2")),
47+
},
48+
},
49+
expected: false,
50+
},
51+
{
52+
name: "resource above maximum should not match",
53+
allocatable: corev1.ResourceList{
54+
corev1.ResourceCPU: resource.MustParse("3"),
55+
},
56+
ranges: fmav1alpha1.ResourceRanges{
57+
corev1.ResourceCPU: fmav1alpha1.ResourceRange{
58+
Max: resourcePtr(resource.MustParse("2")),
59+
},
60+
},
61+
expected: false,
62+
},
63+
{
64+
name: "resource within range should match",
65+
allocatable: corev1.ResourceList{
66+
corev1.ResourceCPU: resource.MustParse("2"),
67+
corev1.ResourceMemory: resource.MustParse("4Gi"),
68+
},
69+
ranges: fmav1alpha1.ResourceRanges{
70+
corev1.ResourceCPU: fmav1alpha1.ResourceRange{
71+
Min: resourcePtr(resource.MustParse("1")),
72+
Max: resourcePtr(resource.MustParse("4")),
73+
},
74+
corev1.ResourceMemory: fmav1alpha1.ResourceRange{
75+
Min: resourcePtr(resource.MustParse("2Gi")),
76+
Max: resourcePtr(resource.MustParse("8Gi")),
77+
},
78+
},
79+
expected: true,
80+
},
81+
{
82+
name: "resource equal to minimum should match",
83+
allocatable: corev1.ResourceList{
84+
corev1.ResourceCPU: resource.MustParse("2"),
85+
},
86+
ranges: fmav1alpha1.ResourceRanges{
87+
corev1.ResourceCPU: fmav1alpha1.ResourceRange{
88+
Min: resourcePtr(resource.MustParse("2")),
89+
},
90+
},
91+
expected: true,
92+
},
93+
{
94+
name: "resource equal to maximum should match",
95+
allocatable: corev1.ResourceList{
96+
corev1.ResourceCPU: resource.MustParse("2"),
97+
},
98+
ranges: fmav1alpha1.ResourceRanges{
99+
corev1.ResourceCPU: fmav1alpha1.ResourceRange{
100+
Max: resourcePtr(resource.MustParse("2")),
101+
},
102+
},
103+
expected: true,
104+
},
105+
{
106+
name: "multiple resources with one out of range should not match",
107+
allocatable: corev1.ResourceList{
108+
corev1.ResourceCPU: resource.MustParse("1"), // below min
109+
corev1.ResourceMemory: resource.MustParse("4Gi"),
110+
},
111+
ranges: fmav1alpha1.ResourceRanges{
112+
corev1.ResourceCPU: fmav1alpha1.ResourceRange{
113+
Min: resourcePtr(resource.MustParse("2")),
114+
},
115+
corev1.ResourceMemory: fmav1alpha1.ResourceRange{
116+
Min: resourcePtr(resource.MustParse("2Gi")),
117+
},
118+
},
119+
expected: false,
120+
},
121+
}
122+
123+
for _, tt := range tests {
124+
t.Run(tt.name, func(t *testing.T) {
125+
result := matchesResourceConditions(tt.allocatable, tt.ranges)
126+
if result != tt.expected {
127+
t.Errorf("matchesResourceConditions() = %v, expected %v", result, tt.expected)
128+
}
129+
})
130+
}
131+
}
132+
133+
// Helper function to create resource.Quantity pointers
134+
func resourcePtr(r resource.Quantity) *resource.Quantity {
135+
return &r
136+
}

0 commit comments

Comments
 (0)