Skip to content

Commit 2555737

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 2555737

11 files changed

Lines changed: 189 additions & 38 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: 38 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,56 @@ 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+
// Create pod source
69+
config := pod.PodScrapingSourceConfig{
70+
ServiceName: pool.EndpointPicker.ServiceName,
71+
ServiceNamespace: pool.EndpointPicker.Namespace,
72+
MetricsPort: pool.EndpointPicker.MetricsPortNumber,
73+
MetricsReaderSecretName: "metrics-reader-secret",
74+
MetricsReaderSecretKey: "token",
75+
}
76+
77+
podSource, err := pod.NewPodScrapingSource(ctx, client, config)
78+
if err != nil {
79+
return err
80+
}
81+
82+
// Register in registry
83+
if err := ds.registry.Register(pool.Name, podSource); err != nil {
84+
return err
85+
}
86+
87+
// Store in the datastore
6088
ds.pools.Store(pool.Name, pool)
6189
return nil
6290
}
@@ -72,6 +100,11 @@ func (ds *datastore) PoolGet(name string) (*poolutil.EndpointPool, error) {
72100
return epp, nil
73101
}
74102

103+
func (ds *datastore) PoolGetMetricsSource(name string) source.MetricsSource {
104+
source := ds.registry.Get(name)
105+
return source
106+
}
107+
75108
func (ds *datastore) PoolGetFromLabels(labels map[string]string) (*poolutil.EndpointPool, error) {
76109
exist := false
77110
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: 130 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,119 @@ 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+
var targetModelID string
202+
pendingRequestExist := false
203+
for _, value := range result.Values {
204+
// Check for pending requests using queue size metrics
205+
metricName := value.Labels["__name__"]
206+
if metricName == "inference_pool_average_queue_size" && value.Value > 0 {
207+
if value.Labels["target_model_name"] != va.Spec.ModelID {
208+
targetModelID = value.Labels["target_model_name"]
209+
ctrl.Log.Info(
210+
"Target workload has pending request, not scaling up", "metricName", metricName,
211+
"metric", value.Labels, "value", value.Value)
212+
pendingRequestExist = true
213+
break
214+
}
215+
}
216+
}
217+
218+
if !pendingRequestExist {
219+
ctrl.Log.Info("No pending request found in the flowcontrol queue - skipping scaling up from zero")
220+
return nil
221+
}
177222

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-
)
223+
// Check if VA modelID matches with EPP request modelID
224+
if va.Spec.ModelID != targetModelID {
225+
ctrl.Log.Info("ModelID mismatch between VA and EPP pending request in the flowcontrol queue", "VA ModelID", va.Spec.ModelID, "EPP ModelID", targetModelID)
226+
return errors.New("ModelID mismatch between VA and EPP pending request in the flowcontrol queue")
227+
}
228+
229+
// 1. Scale up from zero to one
230+
// TO DO: 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.
231+
err = e.Actuator.ScaleTargetObject(ctx, unstructuredObj, int32(targetWorkloadReplicas))
232+
if err != nil {
233+
ctrl.Log.Error(err, "Error scaling up Target Workload", "variant", va.Name, "target VA model", va.Spec.ModelID)
234+
return err
235+
} else {
236+
ctrl.Log.Info("Successfully scaled up Target Workload", "variant", va.Name, "target VA model", va.Spec.ModelID, "inferencepool", pool.EndpointPicker.ServiceName)
237+
}
238+
239+
// 2. Create or update VariantDecision
240+
reason := "Pending request in the inferencePool for target variant model"
241+
decision, hasDecision := common.DecisionCache.Get(va.Name, va.Namespace)
242+
if !hasDecision {
243+
cost, err := strconv.ParseFloat(va.Spec.VariantCost, 64)
244+
if err != nil {
245+
return err
246+
}
247+
common.DecisionCache.Set(va.Name, va.Namespace, interfaces.VariantDecision{
248+
VariantName: va.Name,
249+
Namespace: va.Namespace,
250+
ModelID: va.Spec.ModelID,
251+
Cost: cost,
252+
TargetReplicas: targetWorkloadReplicas, // Scale up to 1 replica
253+
CurrentReplicas: targetWorkloadReplicas,
254+
DesiredReplicas: targetWorkloadReplicas,
255+
LastRunTime: metav1.Now(),
256+
SaturationBased: false,
257+
SafetyOverride: false,
258+
ModelBasedDecision: false,
259+
Reason: reason, // Reason for scaling up
260+
})
261+
} else {
262+
if decision.CurrentReplicas == 0 {
263+
decision.TargetReplicas = targetWorkloadReplicas
264+
decision.CurrentReplicas = targetWorkloadReplicas
265+
decision.DesiredReplicas = targetWorkloadReplicas
266+
decision.LastRunTime = metav1.Now()
267+
decision.SaturationBased = false
268+
decision.SafetyOverride = false
269+
decision.ModelBasedDecision = false
270+
decision.Reason = reason
271+
common.DecisionCache.Set(va.Name, va.Namespace, decision)
272+
} else {
273+
ctrl.Log.Info("WARNING: Target variant decision.CurrentReplicas is not zero", "value", decision.CurrentReplicas)
274+
}
275+
}
276+
277+
// 3. Updates VA status.
278+
// Fetch latest version from API server to avoid conflicts
279+
var updateVa wvav1alpha1.VariantAutoscaling
280+
if err := utils.GetVariantAutoscalingWithBackoff(ctx, e.client, va.Name, va.Namespace, &updateVa); err != nil {
281+
ctrl.Log.Error(err, "Failed to get latest VA from API server", "name", va.Name)
282+
}
283+
// Update DesiredOptimizedAlloc
284+
updateVa.Status.DesiredOptimizedAlloc = wvav1alpha1.OptimizedAlloc{
285+
NumReplicas: targetWorkloadReplicas,
286+
LastRunTime: metav1.Now(),
287+
}
288+
updateVa.Status.Actuation.Applied = true // Reset applied status until Actuator handles it (if needed)
289+
290+
// Set condition based on decision characteristics
291+
wvav1alpha1.SetCondition(&updateVa,
292+
wvav1alpha1.TypeOptimizationReady,
293+
metav1.ConditionTrue,
294+
wvav1alpha1.ReasonOptimizationSucceeded,
295+
fmt.Sprintf("scalefromzero decision: %s", reason))
296+
297+
// 4. Trigger Reconciler
298+
common.DecisionTrigger <- event.GenericEvent{
299+
Object: &updateVa,
300+
}
185301

186-
// TODO: Create EPP source and query metrics port
187302
return nil
188303
}

0 commit comments

Comments
 (0)