Traditional Kubernetes Horizontal Pod Autoscaling (HPA) is inherently reactive, relying on rigid thresholds that often lead to resource underutilization or costly over-provisioning—especially for expensive GPU workloads. In this end-to-end guide, we explore how to implement robust HPA for CPU and GPU workloads, define the critical custom metrics required, and ultimately build an Enterprise Multi-Agent LangGraph RAG system with persistent memory and state to intelligently orchestrate, analyze, and automate these scaling decisions in real-time.

Part 1: Implementing HPA for GPU/CPU Workloads

To scale workloads effectively, Kubernetes requires visibility into resource consumption. While CPU and Memory are native, GPU and application-level metrics require a custom pipeline.

1. The Metrics Pipeline

2. What Custom Metrics Do We Use?

Relying solely on GPU utilization (DCGM_FI_DEV_GPU_UTIL) is a trap; it can spike to 100% while the application is actually bottlenecked by memory or I/O. For enterprise AI/ML workloads, we use a composite of metrics.

Hardware Metrics (via DCGM)

Application/Queue Metrics (via Prometheus/OTel)

Part 2: Real-Time Use Case — Enterprise LLM Inference Platform

The Scenario

An enterprise runs a multi-tenant LLM inference service on Kubernetes using vLLM. Traffic is highly spiky.

The Problem

A standard HPA configured for 80% GPU utilization reacts too slowly to sudden queue spikes, causing SLA breaches. Conversely, during low traffic, it keeps too many pods alive, burning cloud compute budgets.

The Solution

We deploy an AI Control Plane powered by LangGraph. Instead of static HPA rules, a Multi-Agent RAG system continuously analyzes real-time telemetry, retrieves historical scaling post-mortems and documentation (RAG), maintains state across scaling cycles (Memory), and dynamically patches the HPA configurations or triggers node-autoscaling.

Part 3: The LangGraph Multi-Agent RAG Architecture

To build this intelligent control plane, we use LangGraph, which allows us to define cyclical, stateful agent workflows.

The Agents

Telemetry Agent

Queries the Prometheus/K8s API for real-time GPU, CPU, and Queue metrics.

RAG Knowledge Agent

Retrieves context from the enterprise Vector Database (e.g., past scaling incidents, vLLM tuning guides, FinOps budgets).

Decision & Actuation Agent

Synthesizes the telemetry and RAG context, updates the graph state, and generates the K8s HPA patch.

State and Memory

State

A TypedDict tracking current metrics, retrieved documents, and the proposed action.

Memory

We use a persistent Checkpointer (e.g., SQLite/Postgres) so the agents remember past scaling decisions. This prevents "scaling oscillation" (rapidly scaling up and down) by allowing the agent to say, "We scaled up 3 minutes ago, let's wait and observe."

Part 4: Code Implementation

Below is the enterprise-grade Python implementation using langgraph, langchain, and the Kubernetes client.

1. Prerequisites & Imports

import operator
from typing import Annotated, Sequence, TypedDict, Literal
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.sqlite import SqliteSaver
from langchain_community.vectorstores import Chroma
from kubernetes import client, config
import json

# Initialize K8s client (assumes running inside cluster or kubeconfig is loaded)
config.load_incluster_config()
k8s_apps_v1 = client.AppsV1Api()
k8s_autoscaling_v1 = client.AutoscalingV1Api()

2. Defining the State and Memory

We define a strict state schema. The scaling_history acts as our short-term memory within the graph, while the SqliteSaver provides long-term persistent memory across graph invocations.

class AgentState(TypedDict):
    # The current messages in the agent loop
    messages: Annotated[Sequence[BaseMessage], operator.add]

    # Real-time telemetry data
    current_metrics: dict

    # Context retrieved from RAG
    rag_context: str

    # Proposed K8s HPA modifications
    proposed_hpa_patch: dict

    # Memory: History of recent actions to prevent oscillation
    scaling_history: list

    # Final status
    next_action: Literal["scale_up", "scale_down", "hold", "finish"]

3. Building the Agent Nodes

Node A: Telemetry Agent

Fetches real-time data from Prometheus and K8s.

def telemetry_node(state: AgentState):
    # Mocking Prometheus API call for DCGM and Queue metrics
    metrics = {
        "gpu_utilization": 92.5,
        "vram_used_percent": 88.0,
        "inference_queue_depth": 450,
        "p99_latency_ms": 1200
    }

    # Fetch current HPA state
    hpa = k8s_autoscaling_v1.read_namespaced_horizontal_pod_autoscaler(
        name="vllm-inference-hpa",
        namespace="ai-platform"
    )

    metrics["current_replicas"] = hpa.status.current_replicas
    metrics["min_replicas"] = hpa.spec.min_replicas
    metrics["max_replicas"] = hpa.spec.max_replicas

    return {
        "current_metrics": metrics,
        "messages": [AIMessage(content="Telemetry collected.")]
    }

Node B: RAG Knowledge Agent

Queries the Vector DB for enterprise context based on the current metrics.

# Initialize Vector DB (Chroma) with enterprise docs
embeddings = OpenAIEmbeddings()
vectorstore = Chroma(
    collection_name="k8s_finops_and_ops",
    embedding_function=embeddings
)

def rag_node(state: AgentState):
    metrics = state["current_metrics"]

    # Formulate query based on anomalies
    query = (
        f"GPU utilization is {metrics['gpu_utilization']}%, "
        f"Queue depth is {metrics['inference_queue_depth']}. "
    )
    query += (
        "What are the best practices for scaling vLLM under these conditions? "
        "Check past incident reports."
    )

    docs = vectorstore.similarity_search(query, k=3)

    context = "\n---\n".join(
        [doc.page_content for doc in docs]
    )

    return {
        "rag_context": context,
        "messages": [AIMessage(content="RAG context retrieved.")]
    }

Node C: Decision & Actuation Agent (The Orchestrator)

Uses an LLM to decide the next step, utilizing the state and memory.

llm = ChatOpenAI(model="gpt-4o", temperature=0)

def decision_node(state: AgentState):
    prompt = ChatPromptTemplate.from_template("""
    You are an expert Kubernetes FinOps and MLOps AI Agent.

    CURRENT METRICS: {metrics}
    RETRIEVED KNOWLEDGE (RAG): {context}
    RECENT SCALING HISTORY (MEMORY): {history}

    Analyze the situation. If the queue depth is high and GPU is maxed, we need to scale up.
    If queue is empty and GPU is idle, scale down.

    IMPORTANT: Check the scaling_history. If we scaled in the last 3 minutes,
    choose 'hold' to prevent oscillation.

    Respond ONLY in JSON format:
    {{
        "reasoning": "Your thought process",
        "next_action": "scale_up" | "scale_down" | "hold",
        "hpa_patch": {{
            "spec": {{
                "minReplicas": X,
                "maxReplicas": Y
            }}
        }}
    }}
    """)

    chain = prompt | llm

    response = chain.invoke({
        "metrics": json.dumps(state["current_metrics"]),
        "context": state["rag_context"],
        "history": json.dumps(state["scaling_history"][-3:])
    })

    decision = json.loads(response.content)

    # Update history for memory
    history = state["scaling_history"] + [{
        "action": decision["next_action"],
        "metrics": state["current_metrics"]
    }]

    return {
        "proposed_hpa_patch": decision.get("hpa_patch", {}),
        "next_action": decision["next_action"],
        "scaling_history": history,
        "messages": [
            AIMessage(content=f"Decision made: {decision['next_action']}")
        ]
    }

Node D: Actuation Node

Applies the patch to the Kubernetes cluster if a scale event is triggered.

def actuation_node(state: AgentState):
    if state["next_action"] in ["scale_up", "scale_down"]:
        patch = state["proposed_hpa_patch"]

        try:
            k8s_autoscaling_v1.patch_namespaced_horizontal_pod_autoscaler(
                name="vllm-inference-hpa",
                namespace="ai-platform",
                body=patch
            )

            msg = (
                f"Successfully patched HPA. "
                f"Action: {state['next_action']}"
            )

        except Exception as e:
            msg = f"Failed to patch HPA: {str(e)}"

    else:
        msg = "Holding current state. No patch applied."

    return {
        "messages": [AIMessage(content=msg)],
        "next_action": "finish"
    }

4. Compiling the LangGraph with Persistent Memory

Here we wire the nodes together, define the conditional edges, and attach the SqliteSaver for persistent memory.

import sqlite3

def route_decision(state: AgentState):
    if state["next_action"] == "finish":
        return "end"
    return "actuate"

# Initialize persistent memory
# Enterprise grade: use Postgres in production
conn = sqlite3.connect(
    "agent_memory.db",
    check_same_thread=False
)

memory = SqliteSaver(conn)

# Build the Graph
workflow = StateGraph(AgentState)

workflow.add_node("telemetry", telemetry_node)
workflow.add_node("rag", rag_node)
workflow.add_node("decision", decision_node)
workflow.add_node("actuate", actuation_node)

workflow.set_entry_point("telemetry")

workflow.add_edge("telemetry", "rag")
workflow.add_edge("rag", "decision")

workflow.add_conditional_edges(
    "decision",
    route_decision,
    {
        "actuate": "actuate",
        "end": END
    }
)

workflow.add_edge("actuate", END)

# Compile with Checkpointer for Memory
app = workflow.compile(checkpointer=memory)

5. Execution

We run the graph using a thread_id to isolate the memory state for this specific cluster/namespace context.

if __name__ == "__main__":
    config = {
        "configurable": {
            "thread_id": "prod-ai-platform-ns-1"
        }
    }

    initial_state = {
        "messages": [
            HumanMessage(
                content="Initiate scaling evaluation cycle."
            )
        ],
        "current_metrics": {},
        "rag_context": "",
        "proposed_hpa_patch": {},
        "scaling_history": [],
        "next_action": "hold"
    }

    # Invoke the graph
    final_state = app.invoke(initial_state, config)

    print("--- FINAL AGENT OUTPUT ---")

    for message in final_state["messages"]:
        print(message.content)

    print(
        f"Final Action Taken: {final_state['next_action']}"
    )

Conclusion

Implementing HPA for GPU and CPU workloads requires moving beyond basic CPU thresholds into the realm of custom metrics like VRAM utilization and inference queue depth. However, managing these metrics statically is insufficient for modern, spiky AI workloads.

By wrapping the Kubernetes control plane in an Enterprise Multi-Agent LangGraph RAG system, we achieve:

Summary

Modern AI workloads require more than traditional HPA based on CPU or GPU utilization alone. By incorporating custom metrics such as VRAM usage, queue depth, and latency, organizations gain more accurate scaling signals. Combining these metrics with a LangGraph-powered multi-agent RAG architecture enables context-aware, stateful, and memory-driven scaling decisions that improve performance, reduce cloud costs, prevent oscillation, and provide enterprise-grade observability and governance.