Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
accc732
feat(scale-to-zero): add enforcer for scale-to-zero policy enforcement
ev-shindin Jan 19, 2026
026b014
test(unit): add unit tests for scale-to-zero enforcer and collector
ev-shindin Jan 19, 2026
7c4988c
test(e2e): add scale-to-zero e2e tests
ev-shindin Jan 19, 2026
c7caa73
ci: add scale-to-zero e2e test targets to CI pipeline
ev-shindin Jan 19, 2026
d46d144
deploy: add scale-to-zero support to Helm charts and deployment scripts
ev-shindin Jan 19, 2026
11e4e07
test(e2e): add log dumping on test failure for debugging
ev-shindin Jan 19, 2026
cc2a7d3
test: increase requestsPerWorker by 10% to sustain load slightly longer
ev-shindin Jan 19, 2026
19c4595
deploy: default BENCHMARK_MODE to false
ev-shindin Jan 20, 2026
5da44e3
refactor: address review comments for scale-to-zero PR
ev-shindin Jan 20, 2026
63cf440
test: run all e2e tests by default and skip scale-to-zero if feature …
ev-shindin Jan 20, 2026
6a7f2e0
ci: run e2e tests in parallel with scale-to-zero enabled and disabled
ev-shindin Jan 20, 2026
943a70f
test: use unique app label for scale-to-zero test
ev-shindin Jan 20, 2026
35f8ad9
test: fix Eventually assertions in TargetResolved tests
ev-shindin Jan 20, 2026
f800a88
test: increase CI timeout and skip retention period test when feature…
ev-shindin Jan 20, 2026
6030084
test: improve HPAScaleToZero feature gate detection
ev-shindin Jan 20, 2026
e9cd65a
test: increase e2e test timeout and remove redundant targets
ev-shindin Jan 20, 2026
b80a79c
test: wait for load generator to be ready before stability check
ev-shindin Jan 20, 2026
c7009d7
test: enable KV cache metrics in llm-d-sim for e2e tests
ev-shindin Jan 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci-e2e-openshift.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,8 @@ jobs:
# HF_TOKEN is inherited from GITHUB_ENV (set in 'Get HF token from cluster secret' step)
ENVIRONMENT: openshift
INSTALL_GATEWAY_CTRLPLANE: "false"
# Disable benchmark mode - istioBench environment not available in llm-d helmfile
Comment thread
ev-shindin marked this conversation as resolved.
BENCHMARK_MODE: "false"
E2E_TESTS_ENABLED: "true"
NAMESPACE_SCOPED: "false"
# Pass PR-specific namespaces to install script
Expand All @@ -473,6 +475,8 @@ jobs:
CONTROLLER_INSTANCE: ${{ env.WVA_NAMESPACE }}
# vLLM max-num-seqs for e2e testing (lower = easier to saturate)
VLLM_MAX_NUM_SEQS: ${{ env.MAX_NUM_SEQS }}
# Disable benchmark mode - istioBench environment not available in llm-d helmfile
BENCHMARK_MODE: "false"
run: |
echo "Deploying WVA and llm-d infrastructure..."
echo " MODEL_ID: $MODEL_ID"
Expand Down Expand Up @@ -513,6 +517,8 @@ jobs:
# HF_TOKEN is inherited from GITHUB_ENV
ENVIRONMENT: openshift
INSTALL_GATEWAY_CTRLPLANE: "false"
# Disable benchmark mode - istioBench environment not available in llm-d helmfile
BENCHMARK_MODE: "false"
E2E_TESTS_ENABLED: "true"
NAMESPACE_SCOPED: "false"
# Override namespaces for Model B stack
Expand All @@ -526,6 +532,8 @@ jobs:
DEPLOY_HPA: "false"
# vLLM max-num-seqs for e2e testing (lower = easier to saturate)
VLLM_MAX_NUM_SEQS: ${{ env.MAX_NUM_SEQS }}
# Disable benchmark mode - istioBench environment not available in llm-d helmfile
BENCHMARK_MODE: "false"
run: |
echo "Deploying Model B infrastructure in $LLMD_NAMESPACE_B..."
echo " MODEL_ID: $MODEL_ID"
Expand Down
30 changes: 29 additions & 1 deletion .github/workflows/ci-pr-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,32 @@ jobs:
run: |
make test

# E2E tests run in parallel with different HPAScaleToZero feature gate settings
e2e-tests:
runs-on: ubuntu-latest
needs: lint-and-test
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
scale_to_zero: [true, false]
name: e2e-tests (scale-to-zero=${{ matrix.scale_to_zero }})
steps:
- name: Checkout source
uses: actions/checkout@v4

- name: Extract Go version from go.mod
run: sed -En 's/^go (.*)$/GO_VERSION=\1/p' go.mod >> $GITHUB_ENV

- name: Set up Go with cache
uses: actions/setup-go@v5
with:
go-version: "${{ env.GO_VERSION }}"
cache-dependency-path: ./go.sum

- name: Install dependencies
run: go mod download

- name: Install Kind
run: |
ARCH=$(uname -m)
Expand All @@ -57,7 +83,9 @@ jobs:
sudo mv ./kind /usr/local/bin/kind
kind version

- name: Run make test-e2e
- name: Run e2e tests
shell: bash
env:
ENABLE_SCALE_TO_ZERO: ${{ matrix.scale_to_zero }}
run: |
make test-e2e
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ test-e2e: manifests generate fmt vet ## Run the e2e tests. Expected an isolated
echo "Kind is not installed. Please install Kind manually."; \
exit 1; \
}
$(eval FOCUS_ARGS := $(if $(FOCUS),-ginkgo.focus="$(FOCUS)",-ginkgo.focus="Saturation Mode"))
$(eval FOCUS_ARGS := $(if $(FOCUS),-ginkgo.focus="$(FOCUS)",))
$(eval SKIP_ARGS := $(if $(SKIP),-ginkgo.skip="$(SKIP)",))
export COLLECTOR_V2=1 KUBECONFIG=$(KUBECONFIG) K8S_EXPECTED_VERSION=$(K8S_VERSION) && go test ./test/e2e-saturation-based/ -timeout 30m -v -ginkgo.v $(FOCUS_ARGS) $(SKIP_ARGS)
export COLLECTOR_V2=1 KUBECONFIG=$(KUBECONFIG) K8S_EXPECTED_VERSION=$(K8S_VERSION) && go test ./test/e2e-saturation-based/ -timeout 50m -v -ginkgo.v $(FOCUS_ARGS) $(SKIP_ARGS)

# E2E tests on OpenShift cluster
# Supports KUBECONFIG or in-cluster authentication (for self-hosted runners).
Expand All @@ -173,7 +173,7 @@ test-e2e-openshift: ## Run the e2e tests on OpenShift. Supports KUBECONFIG or in
DEPLOYMENT=$(DEPLOYMENT) \
REQUEST_RATE=$(REQUEST_RATE) \
NUM_PROMPTS=$(NUM_PROMPTS) \
go test ./test/e2e-openshift/ -timeout 30m -v -ginkgo.v $(FOCUS_ARGS) $(SKIP_ARGS)
go test ./test/e2e-openshift/ -timeout 50m -v -ginkgo.v $(FOCUS_ARGS) $(SKIP_ARGS)

.PHONY: lint
lint: golangci-lint ## Run golangci-lint linter
Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/variantautoscaling_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ type OptimizedAlloc struct {
Accelerator string `json:"accelerator"`

// NumReplicas is the number of replicas for the optimized allocation.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Minimum=0
NumReplicas int `json:"numReplicas"`
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ spec:
numReplicas:
description: NumReplicas is the number of replicas for the optimized
allocation.
minimum: 1
minimum: 0
type: integer
required:
- accelerator
Expand Down
2 changes: 1 addition & 1 deletion charts/workload-variant-autoscaler/templates/hpa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ spec:
apiVersion: apps/v1
kind: Deployment
name: {{ printf "%s-decode" .Values.llmd.modelName }}
# minReplicas: 0 # scale to zero - alpha feature
minReplicas: {{ .Values.hpa.minReplicas }}
maxReplicas: {{ .Values.hpa.maxReplicas }}
behavior:
scaleUp:
Expand Down
2 changes: 2 additions & 0 deletions charts/workload-variant-autoscaler/values-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ va:
sloTtft: 1000
hpa:
enabled: true
# minReplicas: 0 for scale-to-zero testing (requires HPAScaleToZero feature gate)
minReplicas: 0
maxReplicas: 10
targetAverageValue: "1"
vllmService:
Expand Down
4 changes: 4 additions & 0 deletions charts/workload-variant-autoscaler/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ va:

hpa:
enabled: true
# minReplicas: 0 enables scale-to-zero (requires HPAScaleToZero feature gate in k8s)
# minReplicas: 1 is the safe default that prevents scale-to-zero
# Set to 0 when wva.scaleToZero is enabled
minReplicas: 1
maxReplicas: 10
targetAverageValue: "1"
# HPA scaling behavior configuration
Expand Down
4 changes: 0 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook"

llmdVariantAutoscalingV1alpha1 "github.com/llm-d-incubation/workload-variant-autoscaler/api/v1alpha1"
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/collector/registration"
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/collector/source"
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/collector/source/prometheus"
"github.com/llm-d-incubation/workload-variant-autoscaler/internal/config"
Expand Down Expand Up @@ -379,9 +378,6 @@ func main() {
os.Exit(1)
}

// Register scale-to-zero queries with the prometheus source
registration.RegisterScaleToZeroQueries(sourceRegistry)

engine := saturation.NewEngine(
mgr.GetClient(),
mgr.GetScheme(),
Expand Down
2 changes: 1 addition & 1 deletion config/crd/bases/llmd.ai_variantautoscalings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ spec:
numReplicas:
description: NumReplicas is the number of replicas for the optimized
allocation.
minimum: 1
minimum: 0
type: integer
required:
- accelerator
Expand Down
7 changes: 6 additions & 1 deletion deploy/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ TTFT_AVERAGE_LATENCY_MS=${TTFT_AVERAGE_LATENCY_MS:-200}

# Gateway Configuration
GATEWAY_PROVIDER=${GATEWAY_PROVIDER:-"istio"} # Options: kgateway, istio
BENCHMARK_MODE=${BENCHMARK_MODE:-"true"} # if true, updates to Istio config for benchmark
BENCHMARK_MODE=${BENCHMARK_MODE:-"false"} # if true, updates to Istio config for benchmark (istioBench env required in helmfile)
# Save original value to detect if explicitly set via environment variable
INSTALL_GATEWAY_CTRLPLANE_ORIGINAL="${INSTALL_GATEWAY_CTRLPLANE:-}"
INSTALL_GATEWAY_CTRLPLANE="${INSTALL_GATEWAY_CTRLPLANE:-false}"
Expand All @@ -81,6 +81,9 @@ DEPLOY_PROMETHEUS_ADAPTER=${DEPLOY_PROMETHEUS_ADAPTER:-true}
DEPLOY_VA=${DEPLOY_VA:-true}
DEPLOY_HPA=${DEPLOY_HPA:-true}
HPA_STABILIZATION_SECONDS=${HPA_STABILIZATION_SECONDS:-240}
# HPA minReplicas: 0 enables scale-to-zero (requires HPAScaleToZero feature gate)
# Default to 1 for safety; set to 0 for scale-to-zero testing
HPA_MIN_REPLICAS=${HPA_MIN_REPLICAS:-1}
SKIP_CHECKS=${SKIP_CHECKS:-false}
E2E_TESTS_ENABLED=${E2E_TESTS_ENABLED:-false}
# vLLM max-num-seqs (max concurrent sequences per replica, lower = easier to saturate for testing)
Expand Down Expand Up @@ -145,6 +148,7 @@ Environment Variables:
DEPLOY_VA Deploy VariantAutoscaling (default: true)
DEPLOY_HPA Deploy HPA (default: true)
HPA_STABILIZATION_SECONDS HPA stabilization window in seconds (default: 240)
HPA_MIN_REPLICAS HPA minReplicas (default: 1, set to 0 for scale-to-zero)
UNDEPLOY Undeploy mode (default: false)
DELETE_NAMESPACES Delete namespaces after undeploy (default: false)
CONTROLLER_INSTANCE Controller instance label for multi-controller isolation (optional)
Expand Down Expand Up @@ -427,6 +431,7 @@ deploy_wva_controller() {
--set va.sloTpot=$SLO_TPOT \
--set va.sloTtft=$SLO_TTFT \
--set hpa.enabled=$DEPLOY_HPA \
--set hpa.minReplicas=$HPA_MIN_REPLICAS \
--set hpa.behavior.scaleUp.stabilizationWindowSeconds=$HPA_STABILIZATION_SECONDS \
--set hpa.behavior.scaleDown.stabilizationWindowSeconds=$HPA_STABILIZATION_SECONDS \
--set llmd.namespace=$LLMD_NS \
Expand Down
27 changes: 26 additions & 1 deletion deploy/kind-emulator/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ gpu_type="$DEFAULT_GPU_TYPE"
gpu_model="$DEFAULT_GPU_MODEL"
gpu_memory="$DEFAULT_GPU_MEMORY"
k8s_version="${K8S_VERSION:-$DEFAULT_K8S_VERSION}"
# Enable HPAScaleToZero feature gate (alpha feature for scale-to-zero HPA support)
enable_scale_to_zero="${ENABLE_SCALE_TO_ZERO:-true}"

# --------------------------------------------------------------------
# Cleanup on exit
Expand Down Expand Up @@ -48,7 +50,8 @@ Options:
-h Show this help message

Environment Variables:
K8S_VERSION Kubernetes version to use (default: $DEFAULT_K8S_VERSION)
K8S_VERSION Kubernetes version to use (default: $DEFAULT_K8S_VERSION)
ENABLE_SCALE_TO_ZERO Enable HPAScaleToZero feature gate (default: true)
EOF
}

Expand Down Expand Up @@ -83,6 +86,13 @@ done
# --------------------------------------------------------------------
echo "[1/6] Creating Kind cluster: ${cluster_name} with ${nodes} nodes and ${gpus_per_node} GPUS each..."

# Build feature gates string
feature_gates=""
if [ "$enable_scale_to_zero" = "true" ]; then
feature_gates="HPAScaleToZero=true"
echo " HPAScaleToZero feature gate: enabled"
fi

cat <<EOF > kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
Expand All @@ -91,6 +101,21 @@ nodes:
image: kindest/node:${k8s_version}
EOF

# Add kubeadmConfigPatches for feature gates if any are enabled
if [ -n "$feature_gates" ]; then
cat <<EOF >> kind-config.yaml
kubeadmConfigPatches:
- |
kind: ClusterConfiguration
apiServer:
extraArgs:
feature-gates: ${feature_gates}
controllerManager:
extraArgs:
feature-gates: ${feature_gates}
EOF
fi

for ((i=1; i<nodes; i++)); do
echo "- role: worker" >> kind-config.yaml
echo " image: kindest/node:${k8s_version}" >> kind-config.yaml
Expand Down
60 changes: 60 additions & 0 deletions docs/integrations/hpa-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,66 @@ kubectl -n kube-system get pod -l component=kube-controller-manager -o yaml | gr

7. Specify the `minReplicas: 0` field in the `yaml` snippet for HPA and apply it following the integration steps

### Automatic Setup with Kind Cluster Script

When using the `deploy/kind-emulator/setup.sh` script, the HPAScaleToZero feature gate is **enabled by default**. You can disable it if needed:

```sh
# Default: HPAScaleToZero enabled
./deploy/kind-emulator/setup.sh

# Disable HPAScaleToZero feature gate
ENABLE_SCALE_TO_ZERO=false ./deploy/kind-emulator/setup.sh
```

### OpenShift Cluster Configuration

On OpenShift, the HPAScaleToZero feature gate must be enabled at the cluster level by a cluster administrator. This requires modifying the FeatureGate custom resource:

> **Warning**: Modifying FeatureGate on OpenShift may impact cluster stability as HPAScaleToZero is a TechPreview/alpha feature. Consult your cluster administrator before making these changes.

1. Check current FeatureGate configuration:

```sh
oc get featuregate cluster -o yaml
```

2. Enable HPAScaleToZero feature gate:

```sh
oc patch featuregate cluster --type=merge -p '{
"spec": {
"featureSet": "CustomNoUpgrade",
"customNoUpgrade": {
"enabled": ["HPAScaleToZero"]
}
}
}'
```

3. Wait for the cluster to apply the changes (this may take several minutes as nodes are updated):

```sh
oc get clusterversion -w
```

4. Verify the feature gate is enabled:

```sh
oc get featuregate cluster -o jsonpath='{.spec.customNoUpgrade.enabled}'
```

5. Deploy HPA with `minReplicas: 0`:

```sh
# Using install script
HPA_MIN_REPLICAS=0 ./deploy/install.sh -e openshift

# Or via Helm
helm upgrade workload-variant-autoscaler ./charts/workload-variant-autoscaler \
--set hpa.minReplicas=0
```

### Note on possible timing issues

For this discussion, please refer to the [community doc](https://docs.google.com/document/d/15z1u2HIH7qoxT-nxj4BnZ_TyqHPqIn0FcCPTnIMn7bs/edit?tab=t.0).
Expand Down
Loading