KEDA GPU Scaler

August 14, 2026 · View on GitHub

Scale Kubernetes GPU workloads from real hardware metrics. No DCGM. No PromQL. Optional Prometheus metrics built in.

CI License

A KEDA External Scaler that reads NVIDIA GPU metrics directly from NVML C-bindings and autoscales your vLLM, Triton, and custom inference deployments including scale-to-zero.

Why This Exists

Kubernetes HPA watches CPU and memory. It can't see GPU utilization. Your vLLM pod shows 8% CPU while the GPU is at 100%.

The usual fix is dcgm-exporter → Prometheus → KEDA, but that's 5 components and 15-30s of latency.

This project reads GPU metrics directly from NVML and serves them to KEDA over gRPC. 2 components, 2-4 second latency.

Why Not a Native KEDA Scaler?

Putting GPU support inside KEDA core doesn't work:

  1. CGO Constraint: NVIDIA's Go bindings (go-nvml) require CGO_ENABLED=1. KEDA builds with CGO_ENABLED=0.
  2. Node-Level Hardware Access: The KEDA operator runs as a central pod. NVML requires local GPU device access via libnvidia-ml.so, which only a DaemonSet on GPU nodes can provide.
  3. Independent Release Cycle: Ship GPU scaling improvements without waiting for KEDA release cycles.

This design is documented in KEDA issue #7538.


Architecture

High Level Overview

keda-gpu-scaler architecture overview

Detailed Data Flow

keda-gpu-scaler data flow

  1. DaemonSet — Runs on nodes labeled with nvidia.com/gpu.present: "true".
  2. NVML Bindings — Directly reads Streaming Multiprocessor (SM) utilization and Frame Buffer Memory via go-nvml C-bindings.
  3. gRPC Interface — Implements externalscaler.ExternalScalerServer (IsActive, StreamIsActive, GetMetricSpec, GetMetrics) to natively integrate with the central KEDA operator.
  4. ScaledObject Trigger — Kubernetes deployments scale up/down (including to zero) based on GPU thresholds defined in the ScaledObject.

⚠️ Supported Topology: Each DaemonSet pod reports metrics for its local node only. Multi-node cluster-wide aggregation is not yet implemented — for multi-GPU-node clusters, deploy one ScaledObject per node or use node selectors. See #142 for the roadmap.


GPU Metrics

MetricDescriptionUnit
gpu_utilizationGPU compute (SM) utilization% (0-100)
memory_utilizationGPU memory controller utilization% (0-100)
memory_used_mibGPU VRAM usedMiB
memory_used_percentGPU VRAM used as percentage of total% (0-100)
temperatureGPU die temperatureCelsius
power_drawGPU power consumptionWatts
pcie_tx_kbpsPCIe transmit throughput (CPU→GPU)KB/s
pcie_rx_kbpsPCIe receive throughput (GPU→CPU)KB/s
nvlink_tx_mbpsNVLink transmit throughput (GPU→GPU)MB/s
nvlink_rx_mbpsNVLink receive throughput (GPU→GPU)MB/s
vllm_queue_depthPending requests waiting in the vLLM engine — requires vllmEndpointcount
vllm_kv_cache_usagevLLM GPU KV cache usage — requires vllmEndpoint% (0-100)
triton_queue_wait_msAverage Triton inference queue wait time — requires tritonEndpointms
triton_request_rateTriton inference request rate — requires tritonEndpointrequests/sec

The vllm_* metrics come from the vLLM engine's own /metrics endpoint rather than NVML — see docs/configuration.md. The triton_* metrics likewise come from Triton's own /metrics endpoint and are derived from its cumulative counters — see docs/configuration.md.


Pre-built Scaling Profiles

Instead of configuring raw metric thresholds, use a profile optimized for your workload:

ProfilePrimary MetricTargetActivationUse Case
vllm-inferenceMemory %805vLLM / LLM serving with scale-to-zero
vllm-queue-depthPending requests51vLLM — scale on queue depth via the engine API for faster reaction time
triton-inferenceGPU Util7510NVIDIA Triton Inference Server
triton-queue-waitQueue wait (ms)505Triton — scale on average inference queue wait time via the engine API
triton-request-rateRequests/sec501Triton — scale on inference request rate via the engine API
trainingGPU Util900Training jobs (no scale-to-zero)
batchMemory %701Batch inference with aggressive scale-down
ollamaMemory %703Ollama LLM serving with scale-to-zero
tgi-inferenceMemory %755HuggingFace TGI serving with scale-to-zero
distributed-trainingNVLink TX500005000Data-parallel training on NVLink systems

Prerequisites

  • A Kubernetes cluster (e.g., OKE, GKE, EKS, AKS) with NVIDIA GPU worker nodes
  • KEDA v2.10+ installed in the cluster
  • NVIDIA GPU drivers and Device Plugin installed

Quick Start

1. Deploy the Scaler

Deploy the DaemonSet and gRPC service into your cluster. (Ensure KEDA is already installed.)

kubectl apply -f deploy/manifests.yaml

This deploys a DaemonSet that runs on every GPU node in your cluster, plus a ClusterIP Service for KEDA to discover it.

Or use Helm.

From the published OCI chart (recommended — replace <X.Y.Z> with the latest release):

helm install keda-gpu-scaler \
  oci://ghcr.io/pmady/charts/keda-gpu-scaler --version <X.Y.Z> \
  --namespace keda --create-namespace

From a local checkout:

helm install keda-gpu-scaler deploy/helm/keda-gpu-scaler \
  --namespace keda \
  --set nodeSelector."nvidia\.com/gpu\.present"=true

See the chart README for all configurable values.

2. Attach to your AI Workload

Create a ScaledObject pointing to the external scaler service:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: vllm-inference-scaler
  namespace: ai-workloads
spec:
  scaleTargetRef:
    name: vllm-deepseek-deployment
  minReplicaCount: 1
  maxReplicaCount: 50
  triggers:
    - type: external
      metadata:
        scalerAddress: "keda-gpu-scaler.keda.svc.cluster.local:6000"
        targetGpuUtilization: "80"

Or use a pre-built profile:

triggers:
  - type: external
    metadata:
      scalerAddress: "keda-gpu-scaler.keda.svc.cluster.local:6000"
      profile: "vllm-inference"

3. Custom Configuration

Override any profile default or use raw GPU metrics directly:

triggers:
  - type: external
    metadata:
      scalerAddress: "keda-gpu-scaler.keda.svc.cluster.local:6000"
      metricType: "gpu_utilization"
      targetValue: "85"
      activationThreshold: "10"
      gpuIndex: "0"              # specific GPU index, or omit for all
      aggregation: "max"         # max, min, avg, sum, p95, p99 across GPUs
      cooldownSeconds: "120"    # suppress repeat scale-downs; 0 disables

See deploy/examples/ for ready-to-use ScaledObject manifests.


Configuration Reference

ParameterDescriptionDefault
profilePre-built scaling profile name(none)
metricTypeGPU metric to scale ongpu_utilization
targetValueTarget metric value for scaling80
targetGpuUtilizationShorthand for GPU utilization target(none)
targetMemoryUtilizationShorthand for VRAM utilization target(none)
activationThresholdValue below which scale-to-zero activates0
gpuIndexSpecific GPU index to monitor. Must be -1 (all GPUs) or >= 0; other negative values are rejected-1 (all GPUs)
aggregationMulti-GPU aggregation: max, min, avg, sum, p95, p99max
pollIntervalSecondsMetric polling interval10
cooldownSecondsSuppress repeat scale-downs for this many seconds after a scale-down. Scale-up is never delayed. 0 disables60

Prometheus Metrics (Optional)

The scaler exposes an optional Prometheus-compatible /metrics endpoint for monitoring the scaler itself and GPU fleet health. This is independent of the KEDA scaling path — scaling works identically with or without it.

Enable/Disable

# Enabled by default on port 9090
--metrics-port=9090

# Disable entirely (zero overhead)
--metrics-port=0

Helm:

metrics:
  enabled: true   # set to false to disable
  port: 9090

Exposed Metrics

MetricTypeDescription
keda_gpu_scaler_gpu_utilization_percentGaugeGPU compute utilization (per GPU)
keda_gpu_scaler_gpu_memory_used_bytesGaugeGPU memory in use (per GPU)
keda_gpu_scaler_gpu_memory_total_bytesGaugeTotal GPU memory (per GPU)
keda_gpu_scaler_gpu_temperature_celsiusGaugeGPU temperature (per GPU)
keda_gpu_scaler_gpu_power_draw_wattsGaugeGPU power draw (per GPU)
keda_gpu_scaler_collections_totalCounterTotal NVML collection calls
keda_gpu_scaler_collection_errors_totalCounterFailed NVML collection calls
keda_gpu_scaler_collection_duration_secondsHistogramNVML collection latency
keda_gpu_scaler_scaler_requests_totalCountergRPC requests by method
keda_gpu_scaler_scaler_request_errors_totalCountergRPC errors by method

All per-GPU metrics are labeled with gpu_index, gpu_uuid, and gpu_name.

Kubernetes Probes

The scaler supports two probe mechanisms; the Helm chart defaults to gRPC.

The gRPC server implements the gRPC Health Checking Protocol (google.golang.org/grpc/health, grpc_health_v1) on the server-wide ("") service. A background checker polls NVML (DeviceCount()) every --health-check-interval (default 30s) and reports:

  • SERVING while NVML responds normally.
  • NOT_SERVING if the NVML call fails.
--health-check-interval=30s

Kubernetes 1.24+ can probe this natively:

livenessProbe:
  grpc:
    port: 6000
readinessProbe:
  grpc:
    port: 6000

Helm sets this up automatically (probes.type: grpc, the default).

HTTP probes (legacy / pre-1.24 clusters)

A dedicated HTTP probe port also exposes:

  • /healthz returns 200 while the process is alive.
  • /readyz returns 200 after NVML initializes and the first metrics collection succeeds.
--probe-port=8081

Helm:

probes:
  enabled: true
  type: http
  port: 8081

Build it Yourself

This project requires CGO_ENABLED=1 to compile the NVIDIA C-bindings.

Note

The compiled binaries (keda-gpu-scaler and gpu-metrics) dynamically link NVIDIA's NVML library and load libnvidia-ml.so at runtime. They will fail to start on any machine that does not have the NVIDIA driver installed (which provides libnvidia-ml.so) — for example, a laptop or CI runner with no NVIDIA GPU. You can still build, lint, and run the test suite without a GPU, since the tests use a mock collector (see Can I run this without a GPU?).

# Build KEDA scaler binary (requires CGO for NVML)
make build

# Build standalone GPU metrics CLI (no KEDA/gRPC needed)
make build-metrics

# Build all binaries
make build-all

# Run unit tests
make test

# Run linter
make lint

# Generate protobuf Go code
make proto

# Build and push a release image
make docker-release VERSION=v0.1.0

# Deploy to cluster
make deploy

Checking the Version

Both binaries accept a --version flag (and a bare version argument) that prints the version, Go version, and build date, then exits. Unlike normal operation, this does not require a GPU or the NVIDIA driver:

keda-gpu-scaler --version    # keda-gpu-scaler v0.5.0 (go1.26.4, built 2026-06-25)
gpu-metrics --version        # gpu-metrics v0.5.0 (go1.26.4, built 2026-06-25)

make build stamps the version from git describe at link time; builds without ldflags (e.g. go run) report dev.

Standalone GPU Metrics CLI

Collect GPU metrics without Kubernetes — works on bare metal, SLURM jobs, Flux jobs, Kubernetes pods, and Singularity containers. The same binary and the same JSON schema work everywhere.

Important

gpu-metrics requires libnvidia-ml.so (installed with the NVIDIA driver) on the host. On a machine without an NVIDIA driver it exits immediately with nvml init failed.

gpu-metrics                       # one-shot table output (env auto-detected)
gpu-metrics --format json         # JSON for scripting
gpu-metrics --format csv          # CSV for analysis
gpu-metrics --interval 5s         # continuous collection
gpu-metrics --device 0 --quiet    # single GPU, no logs
gpu-metrics --env slurm           # force environment (auto|k8s|slurm|flux|standalone)
gpu-metrics --version             # print version and exit (no GPU/NVML required)

The --env flag auto-detects the orchestrator by default. Detection priority: SLURM → Flux → Kubernetes → standalone.

Every environment emits the same unified JSON schema with an environment block so you can compare GPU performance across on-prem and cloud with identical tooling:

{
  "environment": { "orchestrator": "slurm", "node": "compute-01", "job_id": "123", "task_rank": 0 },
  "driver_version": "535.104.05",
  "collected_at": "2026-06-17T10:00:00Z",
  "devices": [...]
}

SLURM — auto-detected when SLURM_JOB_ID is set; collects only the GPUs assigned to your job step:

srun --gres=gpu:2 gpu-metrics --format json

Flux — auto-detected when FLUX_JOB_ID is set; collects only the GPUs in CUDA_VISIBLE_DEVICES:

flux run -N1 -g2 gpu-metrics --format json

Or let Flux start and stop collection automatically for the lifetime of a job via the gpu-monitor shell plugin drop-in: flux run -o gpu-monitor -N2 -g1 ./train.sh.

See HPC & Cross-Environment Metrics for full usage, and Cross-Environment Comparison Guide for comparing on-prem vs cloud GPU runs.

Or build the Docker image directly:

docker build -t your-registry/keda-gpu-scaler:v0.1.0 .
docker push your-registry/keda-gpu-scaler:v0.1.0

End-to-End GPU Tests

make test runs the unit suite against a mock collector (no GPU needed). The end-to-end suite is different: it provisions a real GPU cluster on AWS EKS, Azure AKS, or Google GKE with Terratest, installs the NVIDIA GPU operator + KEDA + this scaler, asserts the demo app scales up under GPU load and back down when idle, then tears everything down. The stacks live in infra/terraform/ and the Go suite in tests/terratest/.

⚠️ These tests provision real, billed GPU hardware (~$0.55–1.20/hr per cloud). They are manual-only and each per-cloud job is gated behind a GitHub Environment so a reviewer must approve the spend before it runs.

Running from GitHub Actions

Go to the repo's Actions tab and pick the workflow, then Run workflow:

WorkflowWhat it doesInputs
E2E Cloud Tests (Apply and Destroy)Provisions the cluster, asserts scale up/down, then destroys it.clouds: aws | azure | gcp | all · confirm_cost: type apply to confirm billing
E2E Plan (Manual)Credential-light terraform plan + Infracost estimate — no cluster, no billing.clouds: which stack to plan
E2E Destroy (Manual)Safety net to tear down a cluster a failed run left behind.cloud + exact cluster_name + type destroy

Choosing clouds: the clouds input on the apply workflow fans out to one job per cloud. Pick a single cloud to validate one provider, or all to run AWS, Azure, and GCP in parallel (each still needs its own reviewer approval and GPU quota).

Reading the results

  • Green check on the run = the full contract passed: the scaler DaemonSet came up, the ScaledObject went Ready, the demo app scaled 1 → N under load and N → 1 when idle.
  • Every run uploads an e2e-diagnostics-<cloud>-<run_id> artifact (via if: always()) containing the go test log; on failure it also includes a cluster dump (nodes, scaler pods/logs, ScaledObject, events).
  • The cluster auto-destroys, so to poke a live one, run the suite locally.

Running locally

Requires the cloud CLI authenticated, a remote state backend (see each stack's bootstrap/), and GPU quota in your target region:

cd tests/terratest
E2E_AWS_STATE_BUCKET=<bucket> \
  go test -tags e2e_aws -v -timeout 90m -run TestAWSGPUScalerE2E ./...

Full prerequisites (OIDC/WIF federation, required secrets/variables, GPU quota per cloud) are documented in tests/terratest/README.md.


How It Compares

keda-gpu-scalerdcgm-exporter + PrometheusCustom Metrics API
Components1 DaemonSet (+ optional /metrics)dcgm-exporter + Prometheus + adapterCustom metrics server
Metric latencySub-second (direct NVML)15-30s (scrape interval)Depends on implementation
Scale-to-zeroYes (KEDA native)Yes (with KEDA Prometheus scaler)Manual
Configuration3-line ScaledObjectPromQL query per metricCustom code
GPU metrics10 hardware metrics50+ DCGM metricsWhatever you build
DependenciesKEDA, NVIDIA driversKEDA, Prometheus, dcgm-exporterVaries
Failure domainNode-localCentralized PrometheusVaries

Documentation



Adopters

Using keda-gpu-scaler? Add your organization to ADOPTERS.md.


Roadmap

  • AMD ROCm support
  • MIG per-instance metrics
  • vLLM queue depth scaling — available via the vllm_queue_depth metric type / vllm-queue-depth profile; see docs/configuration.md

Contributors

Thanks to everyone who helps build keda-gpu-scaler.

See CONTRIBUTORS.md for detailed contributions.

Contributing

Contributions welcome — GPU autoscaling use cases, vendor support (AMD ROCm, Intel), or docs improvements. See CONTRIBUTING.md.

Star History

Star History Chart

License

Apache License 2.0. See LICENSE for details.