Skip to content

Commit

Permalink
Merge pull request kruize#1494 from bhanvimenghani/vpa_demo
Browse files Browse the repository at this point in the history
Enables vpa demo to pick up custom term and model
  • Loading branch information
dinogun authored Feb 14, 2025
2 parents a49b7e5 + 2b5b745 commit bca6ab7
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 39 deletions.
12 changes: 6 additions & 6 deletions design/KruizeLocalAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -2572,13 +2572,13 @@ If no experiment type is specified, it will default to `container`.

**Request with `model_settings` and `term_settings` field**

Under `recommendation_settings`, the `model_settings` and `term_settings` field is optional
and can be used to specify model and term details. Currently, it supports configurations
for a single model and a single term. Model can be set to either cost or performance and
term can be set to short, medium or long term.
Under `recommendation_settings`, the `model_settings` and `term_settings` fields are optional
but can be used to specify model and term details. Currently, for monitoring mode user can set
model as cost and/or performance and terms can be set to short, medium and/or long term.
By default, it provides recommendations for all three terms and both models.

If mode is set to auto or recreate and the above settings are not mentioned then it will
default to `performance` and `short` term.
If mode is set to auto or recreate then there can only be one term and one model chosen.
By default, model will be `performance` and term will be set to `short` term.

<details>
<summary><b>Example Request with custom model_settings and term_settings </b></summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@
package com.autotune.analyzer.autoscaler.vpa;

import com.autotune.analyzer.exceptions.ApplyRecommendationsError;
import com.autotune.analyzer.exceptions.InvalidModelException;
import com.autotune.analyzer.exceptions.InvalidTermException;
import com.autotune.analyzer.exceptions.UnableToCreateVPAException;
import com.autotune.analyzer.kruizeObject.KruizeObject;
import com.autotune.analyzer.recommendations.RecommendationConfigItem;
import com.autotune.analyzer.autoscaler.AutoscalerImpl;
import com.autotune.analyzer.recommendations.term.Terms;
import com.autotune.analyzer.recommendations.utils.RecommendationUtils;
import com.autotune.analyzer.utils.AnalyzerConstants;
import com.autotune.analyzer.utils.AnalyzerErrorConstants;
import com.autotune.common.k8sObjects.K8sObject;
import com.autotune.common.data.result.ContainerData;
import com.autotune.analyzer.recommendations.objects.MappedRecommendationForTimestamp;
import com.autotune.analyzer.recommendations.objects.TermRecommendations;
import com.autotune.utils.KruizeConstants;
import io.fabric8.autoscaling.api.model.v1.*;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.Quantity;
Expand Down Expand Up @@ -192,7 +196,7 @@ public void applyResourceRecommendationsForExperiment(KruizeObject kruizeObject)
}

for (K8sObject k8sObject: kruizeObject.getKubernetes_objects()) {
List<RecommendedContainerResources> containerRecommendations = convertRecommendationsToContainerPolicy(k8sObject.getContainerDataMap());
List<RecommendedContainerResources> containerRecommendations = convertRecommendationsToContainerPolicy(k8sObject.getContainerDataMap(), kruizeObject);
if (containerRecommendations.isEmpty()){
LOGGER.error(AnalyzerErrorConstants.RecommendationUpdaterErrors.RECOMMENDATION_DATA_NOT_PRESENT);
} else {
Expand Down Expand Up @@ -223,15 +227,15 @@ public void applyResourceRecommendationsForExperiment(KruizeObject kruizeObject)
}
}
}
} catch (Exception e) {
} catch (Exception | InvalidTermException | InvalidModelException e) {
throw new ApplyRecommendationsError(e.getMessage());
}
}

/**
* This function converts container recommendations for VPA Container Recommendations Object Format
*/
private List<RecommendedContainerResources> convertRecommendationsToContainerPolicy(HashMap<String, ContainerData> containerDataMap) {
private List<RecommendedContainerResources> convertRecommendationsToContainerPolicy(HashMap<String, ContainerData> containerDataMap, KruizeObject kruizeObject) throws InvalidTermException, InvalidModelException {
List<RecommendedContainerResources> containerRecommendations = new ArrayList<>();

for (Map.Entry<String, ContainerData> containerDataEntry : containerDataMap.entrySet()) {
Expand All @@ -249,10 +253,31 @@ private List<RecommendedContainerResources> convertRecommendationsToContainerPo
* The short-term performance recommendations is currently the default for VPA and is hardcoded.
* TODO:// Implement functionality to choose the desired term and model
**/
TermRecommendations termRecommendations = value.getShortTermRecommendations();
HashMap<AnalyzerConstants.ResourceSetting,
HashMap<AnalyzerConstants.RecommendationItem,
RecommendationConfigItem>> recommendationsConfig = termRecommendations.getPerformanceRecommendations().getConfig();
List<Terms> terms = new ArrayList<>(kruizeObject.getTerms().values());
String user_selected_term = terms.get(0).getName();

TermRecommendations termRecommendations;

if (KruizeConstants.JSONKeys.SHORT_TERM.equals(user_selected_term)) {
termRecommendations = value.getShortTermRecommendations();
} else if (KruizeConstants.JSONKeys.MEDIUM_TERM.equals(user_selected_term)) {
termRecommendations = value.getMediumTermRecommendations();
} else if (KruizeConstants.JSONKeys.LONG_TERM.equals(user_selected_term)) {
termRecommendations = value.getLongTermRecommendations();
} else {
throw new IllegalArgumentException("Unknown term: " + user_selected_term);
}
// vpa changes for models
String user_model = kruizeObject.getRecommendation_settings().getModelSettings().getModels().get(0);
HashMap<AnalyzerConstants.ResourceSetting, HashMap<AnalyzerConstants.RecommendationItem, RecommendationConfigItem>> recommendationsConfig;

if (KruizeConstants.JSONKeys.COST.equalsIgnoreCase(user_model)) {
recommendationsConfig = termRecommendations.getCostRecommendations().getConfig();
} else if (KruizeConstants.JSONKeys.PERFORMANCE.equalsIgnoreCase(user_model)) {
recommendationsConfig = termRecommendations.getPerformanceRecommendations().getConfig();
} else {
throw new IllegalArgumentException("Unknown model: "+ user_model);
}

Double cpuRecommendationValue = recommendationsConfig.get(AnalyzerConstants.ResourceSetting.requests).get(AnalyzerConstants.RecommendationItem.CPU).getAmount();
Double memoryRecommendationValue = recommendationsConfig.get(AnalyzerConstants.ResourceSetting.requests).get(AnalyzerConstants.RecommendationItem.MEMORY).getAmount();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*******************************************************************************/
package com.autotune.analyzer.experiment;

import com.autotune.analyzer.exceptions.InvalidModelException;
import com.autotune.analyzer.exceptions.InvalidTermException;
import com.autotune.analyzer.exceptions.KruizeResponse;
import com.autotune.analyzer.kruizeObject.KruizeObject;
import com.autotune.analyzer.serviceObjects.Converters;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,73 @@ public ValidationOutputData validateMandatoryFields(KruizeObject expObj) {
}
);
}


if (AnalyzerConstants.AUTO.equalsIgnoreCase(expObj.getMode()) || AnalyzerConstants.RECREATE.equalsIgnoreCase(expObj.getMode())) {
// only vpa specific check for multiple term & model

if (expObj.getRecommendation_settings().getTermSettings() != null &&
expObj.getRecommendation_settings().getTermSettings().getTerms() != null &&
expObj.getRecommendation_settings().getTermSettings().getTerms().size() > 1) {
// Checks for multiple terms and throws error
errorMsg = AnalyzerErrorConstants.APIErrors.CreateExperimentAPI.MULTIPLE_TERMS_UNSUPPORTED;
validationOutputData.setErrorCode(HttpServletResponse.SC_BAD_REQUEST);
validationOutputData.setSuccess(false);
validationOutputData.setMessage(errorMsg);
return validationOutputData;
}
// Check for multiple models
if (expObj.getRecommendation_settings().getModelSettings() != null &&
expObj.getRecommendation_settings().getModelSettings().getModels() != null &&
expObj.getRecommendation_settings().getModelSettings().getModels().size() > 1) {
errorMsg = AnalyzerErrorConstants.APIErrors.CreateExperimentAPI.MULTIPLE_MODELS_UNSUPPORTED;
validationOutputData.setErrorCode(HttpServletResponse.SC_BAD_REQUEST);
validationOutputData.setSuccess(false);
validationOutputData.setMessage(errorMsg);
return validationOutputData;
}
}

// common check for terms and models
if (expObj.getRecommendation_settings().getTermSettings() != null &&
expObj.getRecommendation_settings().getTermSettings().getTerms() != null ) {
Set<String> validTerms = Set.of(KruizeConstants.JSONKeys.SHORT, KruizeConstants.JSONKeys.MEDIUM, KruizeConstants.JSONKeys.LONG);

for(String term: expObj.getRecommendation_settings().getTermSettings().getTerms()) {
// Check for whitespace in terms
if (term == null || term.trim().isEmpty()) {
errorMsg = AnalyzerErrorConstants.APIErrors.CreateExperimentAPI.WHITESPACE_NOT_ALLOWED;
validationOutputData.setErrorCode(HttpServletResponse.SC_BAD_REQUEST);
validationOutputData.setSuccess(false);
validationOutputData.setMessage(errorMsg);
return validationOutputData;
}
// Check for correct term in terms
if (!validTerms.contains(term)) {
throw new IllegalArgumentException(term + AnalyzerErrorConstants.APIErrors.CreateExperimentAPI.INVALID_TERM_NAME);
}
}
LOGGER.info("All terms are valid");
}

if (expObj.getRecommendation_settings().getModelSettings() != null &&
expObj.getRecommendation_settings().getModelSettings().getModels() != null) {
Set<String> validModels = Set.of(KruizeConstants.JSONKeys.COST, KruizeConstants.JSONKeys.PERFORMANCE);

for (String model: expObj.getRecommendation_settings().getModelSettings().getModels()) {
if (model == null || model.trim().isEmpty()) {
errorMsg = AnalyzerErrorConstants.APIErrors.CreateExperimentAPI.WHITESPACE_NOT_ALLOWED;
validationOutputData.setErrorCode(HttpServletResponse.SC_BAD_REQUEST);
validationOutputData.setSuccess(false);
validationOutputData.setMessage(errorMsg);
return validationOutputData;
}
if (!validModels.contains(model)) {
throw new IllegalArgumentException( model + AnalyzerErrorConstants.APIErrors.CreateExperimentAPI.INVALID_MODEL_NAME);
}
}
}

String depType = "";
if (expObj.getExperiment_usecase_type().isRemote_monitoring()) {
// In case of RM, kubernetes_obj is mandatory
Expand Down
39 changes: 29 additions & 10 deletions src/main/java/com/autotune/analyzer/kruizeObject/KruizeObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,15 @@ public static void setDefaultTerms(Map<String, Terms> terms, KruizeObject kruize
// for monitoring use case
terms.put(KruizeConstants.JSONKeys.SHORT_TERM, new Terms(KruizeConstants.JSONKeys.SHORT_TERM,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.SHORT_TERM_DURATION_DAYS,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.SHORT_TERM_DURATION_DAYS_THRESHOLD,
getTermThresholdInDays(KruizeConstants.JSONKeys.SHORT_TERM, kruizeObject.getTrial_settings().getMeasurement_durationMinutes_inDouble()),
4, 0.25));
terms.put(KruizeConstants.JSONKeys.MEDIUM_TERM, new Terms(KruizeConstants.JSONKeys.MEDIUM_TERM,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.MEDIUM_TERM_DURATION_DAYS,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.MEDIUM_TERM_DURATION_DAYS_THRESHOLD,
getTermThresholdInDays(KruizeConstants.JSONKeys.MEDIUM_TERM, kruizeObject.getTrial_settings().getMeasurement_durationMinutes_inDouble()),
7, 1));
terms.put(KruizeConstants.JSONKeys.LONG_TERM, new Terms(KruizeConstants.JSONKeys.LONG_TERM,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.LONG_TERM_DURATION_DAYS,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.LONG_TERM_DURATION_DAYS_THRESHOLD,
getTermThresholdInDays(KruizeConstants.JSONKeys.LONG_TERM, kruizeObject.getTrial_settings().getMeasurement_durationMinutes_inDouble()),
15, 1));
kruizeObject.setTerms(terms);

Expand All @@ -161,7 +161,7 @@ public static void setDefaultTermsForAutoAndRecreate(Map<String, Terms> terms, K
// Default is Short Term
terms.put(KruizeConstants.JSONKeys.SHORT_TERM, new Terms(KruizeConstants.JSONKeys.SHORT_TERM,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.SHORT_TERM_DURATION_DAYS,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.SHORT_TERM_DURATION_DAYS_THRESHOLD,
getTermThresholdInDays(KruizeConstants.JSONKeys.SHORT_TERM, kruizeObject.getTrial_settings().getMeasurement_durationMinutes_inDouble()),
4, 0.25));

kruizeObject.setTerms(terms);
Expand All @@ -176,20 +176,20 @@ public static void setCustomTerms(Map<String, Terms> terms, KruizeObject kruizeO
List<String> termList = kruizeObject.getRecommendation_settings().getTermSettings().getTerms();

for (String userInputTerm : termList) {
if (AnalyzerConstants.RecommendationSettings.SHORT.equalsIgnoreCase(userInputTerm)) {
if (KruizeConstants.JSONKeys.SHORT.equalsIgnoreCase(userInputTerm)) {
terms.put(KruizeConstants.JSONKeys.SHORT_TERM, new Terms(KruizeConstants.JSONKeys.SHORT_TERM,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.SHORT_TERM_DURATION_DAYS,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.SHORT_TERM_DURATION_DAYS_THRESHOLD,
getTermThresholdInDays(KruizeConstants.JSONKeys.SHORT_TERM, kruizeObject.getTrial_settings().getMeasurement_durationMinutes_inDouble()),
4, 0.25));
} else if (AnalyzerConstants.RecommendationSettings.MEDIUM.equalsIgnoreCase(userInputTerm)) {
} else if (KruizeConstants.JSONKeys.MEDIUM.equalsIgnoreCase(userInputTerm)) {
terms.put(KruizeConstants.JSONKeys.MEDIUM_TERM, new Terms(KruizeConstants.JSONKeys.MEDIUM_TERM,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.MEDIUM_TERM_DURATION_DAYS,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.MEDIUM_TERM_DURATION_DAYS_THRESHOLD,
getTermThresholdInDays(KruizeConstants.JSONKeys.MEDIUM_TERM, kruizeObject.getTrial_settings().getMeasurement_durationMinutes_inDouble()),
7, 1));
} else if (AnalyzerConstants.RecommendationSettings.LONG.equalsIgnoreCase(userInputTerm)) {
} else if (KruizeConstants.JSONKeys.LONG.equalsIgnoreCase(userInputTerm)) {
terms.put(KruizeConstants.JSONKeys.LONG_TERM, new Terms(KruizeConstants.JSONKeys.LONG_TERM,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.LONG_TERM_DURATION_DAYS,
KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.LONG_TERM_DURATION_DAYS_THRESHOLD,
getTermThresholdInDays(KruizeConstants.JSONKeys.LONG_TERM, kruizeObject.getTrial_settings().getMeasurement_durationMinutes_inDouble()),
15, 1));
} else {
throw new InvalidTermException(userInputTerm + AnalyzerErrorConstants.APIErrors.CreateExperimentAPI.INVALID_TERM_NAME);
Expand Down Expand Up @@ -412,4 +412,23 @@ public boolean isNamespaceExperiment() {
public boolean isContainerExperiment() {
return ExperimentTypeUtil.isContainerExperiment(experimentType);
}

private static double getTermThresholdInDays(String term, Double measurement_duration) {
double minDataPoints = 2;

switch (term) {
case KruizeConstants.JSONKeys.SHORT_TERM:
minDataPoints = KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.SHORT_TERM_MIN_DATAPOINTS;
break;
case KruizeConstants.JSONKeys.MEDIUM_TERM:
minDataPoints = KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.MEDIUM_TERM_MIN_DATAPOINTS;
break;
case KruizeConstants.JSONKeys.LONG_TERM:
minDataPoints = KruizeConstants.RecommendationEngineConstants.DurationBasedEngine.DurationAmount.LONG_TERM_MIN_DATAPOINTS;
break;
}

return ((double) measurement_duration * minDataPoints
/ (KruizeConstants.TimeConv.NO_OF_HOURS_PER_DAY * KruizeConstants.TimeConv.NO_OF_MINUTES_PER_HOUR));
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
/*******************************************************************************
* Copyright (c) 2025 Red Hat, IBM Corporation and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package com.autotune.analyzer.kruizeObject;

import java.util.List;
Expand Down
Loading

0 comments on commit bca6ab7

Please sign in to comment.