@@ -19,6 +19,8 @@ package scalefromzero
1919import (
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-
4348type 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