Skip to content

Commit a53f130

Browse files
committed
Adding integration with pod scraping source and e2e tests
Signed-off-by: Braulio Dumba <Braulio.Dumba@ibm.com>
1 parent 6efe4b0 commit a53f130

11 files changed

Lines changed: 337 additions & 39 deletions

File tree

charts/workload-variant-autoscaler/templates/rbac/role.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ rules:
5050
- secrets
5151
verbs:
5252
- get
53+
- list
54+
- watch
5355
- apiGroups:
5456
- apps
5557
resources:

cmd/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,10 +426,11 @@ func main() {
426426
}
427427
// +kubebuilder:scaffold:builder
428428

429+
// Create InferencePool reconciler
429430
// Create InferencePool reconciler
430431
inferencePoolReconciler := &controller.InferencePoolReconciler{
431432
Datastore: ds,
432-
Reader: mgr.GetClient(),
433+
Client: mgr.GetClient(),
433434
PoolGKNN: poolutil.DefaultPoolGKNN(),
434435
}
435436

internal/actuator/direct_actuator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package actuator
1919
import (
2020
"context"
2121

22-
poolutil "github.com/llm-d-incubation/workload-variant-autoscaler/internal/engines/scalefromzero"
22+
poolutil "github.com/llm-d-incubation/workload-variant-autoscaler/internal/utils/pool"
2323
autoscalingv1 "k8s.io/api/autoscaling/v1"
2424
"k8s.io/apimachinery/pkg/api/meta"
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

internal/controller/inferencepool_reconciler.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import (
3333
)
3434

3535
type InferencePoolReconciler struct {
36-
client.Reader
36+
client.Client
3737
Datastore datastore.Datastore
3838
PoolGKNN common.GKNN
3939
}
@@ -80,12 +80,12 @@ func (c *InferencePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques
8080

8181
switch pool := obj.(type) {
8282
case *v1.InferencePool:
83-
endpointPool, err = poolutils.InferencePoolToEndpointPool(ctx, c.Reader, pool)
83+
endpointPool, err = poolutils.InferencePoolToEndpointPool(ctx, c.Client, pool)
8484
if err != nil {
8585
return ctrl.Result{}, fmt.Errorf("failed to convert InferencePool v1 to EndPointPool - %w", err)
8686
}
8787
case *v1alpha2.InferencePool:
88-
endpointPool, err = poolutils.AlphaInferencePoolToEndpointPool(ctx, c.Reader, pool)
88+
endpointPool, err = poolutils.AlphaInferencePoolToEndpointPool(ctx, c.Client, pool)
8989
if err != nil {
9090
return ctrl.Result{}, fmt.Errorf("failed to convert InferencePool v1alpha2 to EndPointPool - %w", err)
9191
}
@@ -94,7 +94,7 @@ func (c *InferencePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques
9494
}
9595

9696
if endpointPool != nil {
97-
if err := c.Datastore.PoolSet(endpointPool); err != nil {
97+
if err := c.Datastore.PoolSet(ctx, c.Client, endpointPool); err != nil {
9898
return ctrl.Result{}, fmt.Errorf("failed to add endpoint into the datastore: - %w", err)
9999
}
100100
}

internal/controller/inferencepool_reconciler_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func TestInferencePoolReconcile(t *testing.T) {
101101
ctx := context.Background()
102102

103103
ds := datastore.NewDatastore()
104-
inferencePoolReconciler := &InferencePoolReconciler{Reader: fakeClient, Datastore: ds, PoolGKNN: gknn}
104+
inferencePoolReconciler := &InferencePoolReconciler{Client: fakeClient, Datastore: ds, PoolGKNN: gknn}
105105

106106
if _, err := inferencePoolReconciler.Reconcile(ctx, req); err != nil {
107107
t.Errorf("Unexpected InferencePool reconcile error: %v", err)
@@ -222,7 +222,7 @@ func TestAlphaInferencePoolReconcile(t *testing.T) {
222222
ctx := context.Background()
223223

224224
ds := datastore.NewDatastore()
225-
inferencePoolReconciler := &InferencePoolReconciler{Reader: fakeClient, Datastore: ds, PoolGKNN: gknn}
225+
inferencePoolReconciler := &InferencePoolReconciler{Client: fakeClient, Datastore: ds, PoolGKNN: gknn}
226226

227227
if _, err := inferencePoolReconciler.Reconcile(ctx, req); err != nil {
228228
t.Errorf("Unexpected InferencePool reconcile error: %v", err)

internal/datastore/datastore.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ limitations under the License.
1717
package datastore
1818

1919
import (
20+
"context"
2021
"errors"
2122
"sync"
2223

24+
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/collector/source"
25+
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/collector/source/pod"
2326
poolutil "github.com/llm-d-incubation/workload-variant-autoscaler/internal/utils/pool"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
2428
)
2529

2630
var (
@@ -31,32 +35,59 @@ var (
3135
// The datastore is a local cache of relevant data for the given InferencePool (currently all pulled from k8s-api)
3236
type Datastore interface {
3337
// InferencePool operations
34-
PoolSet(pool *poolutil.EndpointPool) error
38+
PoolSet(ctx context.Context, client client.Client, pool *poolutil.EndpointPool) error
3539
PoolGet(name string) (*poolutil.EndpointPool, error)
40+
PoolGetMetricsSource(name string) source.MetricsSource
3641
PoolList() []*poolutil.EndpointPool
3742
PoolGetFromLabels(labels map[string]string) (*poolutil.EndpointPool, error)
38-
PoolDelete(poolName string)
43+
PoolDelete(name string)
3944

4045
// Clears the store state, happens when the pool gets deleted.
4146
Clear()
4247
}
4348

4449
func NewDatastore() Datastore {
4550
store := &datastore{
46-
pools: &sync.Map{},
51+
pools: &sync.Map{},
52+
registry: source.NewSourceRegistry(),
4753
}
4854
return store
4955
}
5056

5157
type datastore struct {
52-
pools *sync.Map
58+
pools *sync.Map
59+
registry *source.SourceRegistry
5360
}
5461

5562
// Datastore operations
56-
func (ds *datastore) PoolSet(pool *poolutil.EndpointPool) error {
63+
func (ds *datastore) PoolSet(ctx context.Context, client client.Client, pool *poolutil.EndpointPool) error {
5764
if pool == nil {
5865
return errPoolIsNull
5966
}
67+
68+
if ds.registry.Get(pool.Name) == nil {
69+
// Create pod source
70+
config := pod.PodScrapingSourceConfig{
71+
ServiceName: pool.EndpointPicker.ServiceName,
72+
ServiceNamespace: pool.EndpointPicker.Namespace,
73+
MetricsPort: pool.EndpointPicker.MetricsPortNumber,
74+
MetricsReaderSecretName: "metrics-reader-secret",
75+
MetricsReaderSecretKey: "token",
76+
}
77+
78+
podSource, err := pod.NewPodScrapingSource(ctx, client, config)
79+
if err != nil {
80+
return err
81+
}
82+
83+
// Register in registry
84+
// TODO: We need to be able to update or delete a pod source object in the registry at internal/collector/source/registry.go
85+
if err := ds.registry.Register(pool.Name, podSource); err != nil {
86+
return err
87+
}
88+
}
89+
90+
// Store in the datastore
6091
ds.pools.Store(pool.Name, pool)
6192
return nil
6293
}
@@ -72,6 +103,11 @@ func (ds *datastore) PoolGet(name string) (*poolutil.EndpointPool, error) {
72103
return epp, nil
73104
}
74105

106+
func (ds *datastore) PoolGetMetricsSource(name string) source.MetricsSource {
107+
source := ds.registry.Get(name)
108+
return source
109+
}
110+
75111
func (ds *datastore) PoolGetFromLabels(labels map[string]string) (*poolutil.EndpointPool, error) {
76112
exist := false
77113
var ep *poolutil.EndpointPool

internal/datastore/datastore_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func TestDatastore(t *testing.T) {
9494
}
9595

9696
// Test PoolSet
97-
gotErr := ds.PoolSet(ep)
97+
gotErr := ds.PoolSet(ctx, fakeClient, ep)
9898
if diff := cmp.Diff(tt.wantErr, gotErr, cmpopts.EquateErrors()); diff != "" {
9999
t.Errorf("Unexpected error diff (+got/-want): %s", diff)
100100
}
@@ -136,7 +136,7 @@ func TestDatastore(t *testing.T) {
136136
ds.PoolDelete(ep.Name)
137137
assert.Equal(t, len(ds.PoolList()), tt.clearDeleteResultLen, "Pools map should have the expected length after item deleted")
138138

139-
if err := ds.PoolSet(ep); err != nil {
139+
if err := ds.PoolSet(ctx, fakeClient, ep); err != nil {
140140
t.Errorf("failed to add endpoint into the datastore: %v", err)
141141
}
142142
assert.Equal(t, len(ds.PoolList()), tt.listResultLen, "Pools map should have the expected length after item added")

internal/engines/scalefromzero/engine.go

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package scalefromzero
1919
import (
2020
"context"
2121
"errors"
22+
"fmt"
23+
"strconv"
2224
"sync"
2325
"time"
2426

@@ -29,22 +31,26 @@ import (
2931
"k8s.io/client-go/rest"
3032
ctrl "sigs.k8s.io/controller-runtime"
3133
"sigs.k8s.io/controller-runtime/pkg/client"
34+
"sigs.k8s.io/controller-runtime/pkg/event"
3235

3336
wvav1alpha1 "github.com/llm-d-incubation/workload-variant-autoscaler/api/v1alpha1"
37+
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/actuator"
38+
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/collector/source"
3439
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/datastore"
40+
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/engines/common"
3541
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/engines/executor"
42+
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/interfaces"
3643
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/logging"
3744
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/utils"
45+
poolutil "github.com/llm-d-incubation/workload-variant-autoscaler/internal/utils/pool"
3846
)
3947

40-
// NOTE: This is a placeholder for the scale-from-zero engine implementation.
41-
// The actual logic for the scale-from-zero engine should be implemented here.
42-
4348
type Engine struct {
4449
client client.Client
4550
executor executor.Executor
4651
Datastore datastore.Datastore
4752
DynamicClient dynamic.Interface
53+
Actuator *actuator.DirectActuator
4854
Mapper meta.RESTMapper
4955
}
5056

@@ -55,10 +61,16 @@ func NewEngine(client client.Client, mapper meta.RESTMapper, config *rest.Config
5561
return nil, err
5662
}
5763

64+
actuator, err := actuator.NewDirectActuator(config)
65+
if err != nil {
66+
return nil, err
67+
}
68+
5869
engine := Engine{
5970
client: client,
6071
Datastore: ds,
6172
DynamicClient: dynamicClient,
73+
Actuator: actuator,
6274
Mapper: mapper,
6375
}
6476

@@ -110,7 +122,7 @@ func (e *Engine) optimize(ctx context.Context) error {
110122
defer wg.Done()
111123
defer func() { <-sem }()
112124

113-
err := e.processInactiveVariant(ctx, va)
125+
err := e.processInactiveVariant(ctx, va, 1)
114126
if err != nil {
115127
ctrl.Log.V(logging.DEBUG).Error(err, "Error Processing variant", "name", va.Name)
116128
errorCh <- err
@@ -141,13 +153,13 @@ func (e *Engine) optimize(ctx context.Context) error {
141153
}
142154

143155
// ProcessInactiveVariant processes a single inactive VariantAutoscaling resource.
144-
func (e *Engine) processInactiveVariant(ctx context.Context, va wvav1alpha1.VariantAutoscaling) error {
156+
func (e *Engine) processInactiveVariant(ctx context.Context, va wvav1alpha1.VariantAutoscaling, targetWorkloadReplicas int) error {
145157
objAPI := va.GetScaleTargetAPI()
146158
objKind := va.GetScaleTargetKind()
147159
objName := va.GetScaleTargetName()
148160

149161
// Parse Group, Version, Kind, Resource
150-
gvr, err := GetResourceForKind(e.Mapper, objAPI, objKind)
162+
gvr, err := poolutil.GetResourceForKind(e.Mapper, objAPI, objKind)
151163
if err != nil {
152164
return err
153165
}
@@ -173,16 +185,111 @@ func (e *Engine) processInactiveVariant(ctx context.Context, va wvav1alpha1.Vari
173185
return err
174186
}
175187

176-
epp := pool.EndpointPicker
188+
// Use EPP source from registry
189+
eppSource := e.Datastore.PoolGetMetricsSource(pool.Name)
190+
if eppSource == nil {
191+
return errors.New("endpointpicker metrics source not found in datastore")
192+
}
193+
194+
results, err := eppSource.Refresh(ctx, source.RefreshSpec{})
195+
if err != nil {
196+
return err
197+
}
198+
199+
// Check if there are pending request in the EPP flowcontrol queue for target workload VA modelID
200+
result := results["all_metrics"]
201+
pendingRequestExist := false
202+
for _, value := range result.Values {
203+
// Check for pending requests using queue size metrics
204+
metricName := value.Labels["__name__"]
205+
if metricName == "inference_pool_average_queue_size" && value.Value > 0 {
206+
if value.Labels["target_model_name"] == va.Spec.ModelID {
207+
ctrl.Log.Info(
208+
"Target workload has pending requests, not scaling up", "metricName", metricName,
209+
"metric", value.Labels, "value", value.Value)
210+
pendingRequestExist = true
211+
break
212+
}
213+
}
214+
}
177215

178-
// For Tests only (REMOVE LATER)
179-
ctrl.Log.V(logging.DEBUG).Info(
180-
"Target EndpointPicker resolved for inactive variant",
181-
"service", epp.ServiceName,
182-
"namespace", epp.Namespace,
183-
"metricsPort", epp.MetricsPortNumber,
184-
)
216+
if !pendingRequestExist {
217+
ctrl.Log.Info("No pending requests found in the flowcontrol queue - skipping scaling up from zero")
218+
return nil
219+
}
220+
221+
// 1. Scale up from zero to one
222+
// TODO: Right now we are scaling all the VA for the same target model. We need to scale only the VA that has the lowest cost.
223+
err = e.Actuator.ScaleTargetObject(ctx, unstructuredObj, int32(targetWorkloadReplicas))
224+
if err != nil {
225+
ctrl.Log.Error(err, "Error scaling up Target Workload", "variant", va.Name, "target VA model", va.Spec.ModelID)
226+
return err
227+
} else {
228+
ctrl.Log.Info("Successfully scaled up Target Workload", "variant", va.Name, "target VA model", va.Spec.ModelID, "inferencepool", pool.EndpointPicker.ServiceName)
229+
}
230+
231+
// 2. Create or update VariantDecision
232+
reason := "Pending request in the inferencePool for target variant model"
233+
decision, hasDecision := common.DecisionCache.Get(va.Name, va.Namespace)
234+
if !hasDecision {
235+
cost, err := strconv.ParseFloat(va.Spec.VariantCost, 64)
236+
if err != nil {
237+
return err
238+
}
239+
common.DecisionCache.Set(va.Name, va.Namespace, interfaces.VariantDecision{
240+
VariantName: va.Name,
241+
Namespace: va.Namespace,
242+
ModelID: va.Spec.ModelID,
243+
Cost: cost,
244+
TargetReplicas: targetWorkloadReplicas, // Scale up to 1 replica
245+
CurrentReplicas: targetWorkloadReplicas,
246+
DesiredReplicas: targetWorkloadReplicas,
247+
LastRunTime: metav1.Now(),
248+
SaturationBased: false,
249+
SafetyOverride: false,
250+
ModelBasedDecision: false,
251+
Reason: reason, // Reason for scaling up
252+
})
253+
} else {
254+
if decision.CurrentReplicas == 0 {
255+
decision.TargetReplicas = targetWorkloadReplicas
256+
decision.CurrentReplicas = targetWorkloadReplicas
257+
decision.DesiredReplicas = targetWorkloadReplicas
258+
decision.LastRunTime = metav1.Now()
259+
decision.SaturationBased = false
260+
decision.SafetyOverride = false
261+
decision.ModelBasedDecision = false
262+
decision.Reason = reason
263+
common.DecisionCache.Set(va.Name, va.Namespace, decision)
264+
} else {
265+
ctrl.Log.Info("WARNING: Target variant decision.CurrentReplicas is not zero", "value", decision.CurrentReplicas)
266+
}
267+
}
268+
269+
// 3. Updates VA status.
270+
// Fetch latest version from API server to avoid conflicts
271+
var updateVa wvav1alpha1.VariantAutoscaling
272+
if err := utils.GetVariantAutoscalingWithBackoff(ctx, e.client, va.Name, va.Namespace, &updateVa); err != nil {
273+
ctrl.Log.Error(err, "Failed to get latest VA from API server", "name", va.Name)
274+
}
275+
// Update DesiredOptimizedAlloc
276+
updateVa.Status.DesiredOptimizedAlloc = wvav1alpha1.OptimizedAlloc{
277+
NumReplicas: targetWorkloadReplicas,
278+
LastRunTime: metav1.Now(),
279+
}
280+
updateVa.Status.Actuation.Applied = true // Reset applied status until Actuator handles it (if needed)
281+
282+
// Set condition based on decision characteristics
283+
wvav1alpha1.SetCondition(&updateVa,
284+
wvav1alpha1.TypeOptimizationReady,
285+
metav1.ConditionTrue,
286+
wvav1alpha1.ReasonOptimizationSucceeded,
287+
fmt.Sprintf("scalefromzero decision: %s", reason))
288+
289+
// 4. Trigger Reconciler
290+
common.DecisionTrigger <- event.GenericEvent{
291+
Object: &updateVa,
292+
}
185293

186-
// TODO: Create EPP source and query metrics port
187294
return nil
188295
}

0 commit comments

Comments
 (0)