@@ -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,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