Ecstaticloud
Initializing0%
Ecstaticloud Logo
Back to Insights
AI & CloudSeptember 8, 2026

Optimizing Distributed LLM Inference Latency on Kubernetes with vLLM and Triton

Scaling large language model inference in production often bottlenecks on GPU memory fragmentation and unpredictable tail latency. This guide demonstrates how combining vLLM's PagedAttention with custom Triton inference servers on Kubernetes can cut latency by 40% while drastically lowering cloud GPU costs.

Deploying Large Language Models (LLMs) like LLaMA 3, Mixtral, or Qwen in production introduces a stark operational reality: standard model-serving paradigms fail under high-concurrency, long-context workloads. While scaling out GPU instances on Kubernetes seems like an easy fix, it quickly leads to skyrocketing cloud bills and severe performance bottlenecks.

In high-concurrency environments, over 60% to 80% of GPU VRAM can be wasted on memory fragmentation caused by the Key-Value (KV) cache. This memory bloat directly starves batch sizes, causes request queuing, and drives P99 tail latencies (both Time-To-First-Token and Time-Per-Output-Token) into unacceptable ranges.

By combining vLLM's PagedAttention engine with Triton Inference Server orchestrated on Kubernetes, you can build a highly performant, production-grade serving stack. This architecture can achieve up to a 40% reduction in P99 latency while simultaneously maximizing GPU memory utilization—allowing you to pack significantly higher throughput into fewer expensive GPU nodes.


The Bottleneck: Why Traditional LLM Serving Fails

To understand why this hybrid stack is necessary, we must analyze where traditional serving frameworks fail during LLM autoregressive decoding.

1. KV Cache Memory Fragmentation

During inference, an LLM generates tokens sequentially. To avoid recomputing past tokens, the key and value states for every token in the sequence are cached in GPU memory (the KV Cache).

In standard frameworks (like naive PyTorch pipelines or early HuggingFace deployments), memory for the KV Cache must be allocated contiguously for the maximum possible sequence length (e.g., 8,192 or 32,768 tokens). Because actual request output lengths vary wildly, huge swathes of allocated VRAM remain unused, yet locked. This causes severe internal and external memory fragmentation, forcing developers to reduce batch sizes to prevent Out-Of-Memory (OOM) crashes.

Naive Contiguous Allocation (Massive Memory Waste):
[ Req 1: Generated 256 tokens | Locked / Unused Allocation up to 4096 tokens... ]
[ Req 2: Generated 1024 tokens | Locked / Unused Allocation up to 4096 tokens... ]

PagedAttention Allocation (Non-Contiguous Virtual Paging):
[ Block 1 ][ Block 7 ][ Block 3 ] -> Dynamically assigned physical GPU pages

2. High P99 Tail Latency (TTFT vs. TPOT)

LLM latency breaks down into two core metrics:

  • Time-To-First-Token (TTFT): Latency during the prefill phase, processing the initial input prompt in parallel.
  • Time-Per-Output-Token (TPOT): Latency during the decode phase, generating tokens one by one per sequence.

When a massive prefill request arrives while multiple requests are in the middle of their decode phase, the compute-heavy prefill blocks the bandwidth-heavy decodes. Without dynamic, chunked request scheduling, your P99 tail latency spikes dramatically.


Architectural Deep Dive: vLLM + Triton on Kubernetes

To solve these hardware-level starvation problems, we decouple network transport, scheduling, and tensor calculations using a layered stack.

                       +-------------------------------------------------+
                       |             Kubernetes Ingress (gRPC/HTTP)      |
                       +-------------------------------------------------+
                                                |
                                                v
                       +-------------------------------------------------+
                       |         Triton Inference Server Pod             |
                       |  +-------------------------------------------+  |
                       |  | Triton Frontend (Dynamic Batching / IPC) |  |
                       |  +-------------------------------------------+  |
                       |                        |                        |
                       |                        v                        |
                       |  +-------------------------------------------+  |
                       |  | Custom Python Backend Engine (vLLM)       |  |
                       |  | - PagedAttention Virtual Manager          |  |
                       |  | - Continuous Batching Scheduler           |  |
                       |  | - Decoupled Streaming Output              |  |
                       |  +-------------------------------------------+  |
                       |                        |                        |
                       |                        v                        |
                       |  +-------------------------------------------+  |
                       |  | CUDA Kernels / NCCL Model Parallelism     |  |
                       |  +-------------------------------------------+  |
                       +-------------------------------------------------+
                                                |
                                                v
                       +-------------------------------------------------+
                       | GPU Hardware Layer (e.g., 4x NVIDIA A100/H100)  |
                       +-------------------------------------------------+

How the Components Work Together:

  1. vLLM Core (PagedAttention): Implements virtual memory management similar to OS paging for the KV Cache. It breaks KV caches into physical blocks, mapping logical token pages non-contiguously in GPU VRAM. This reduces KV cache waste down to under 4%.
  2. Triton Inference Server: Acts as the outer production harness. It provides high-performance gRPC/HTTP protocol handlers, dynamic request queue management, concurrent execution contexts, and decoupled streaming response capabilities native to C++/Python IPC channels.
  3. Kubernetes Infrastructure: Orchestrates host topology, binds local fast NVMe storage for rapid checkpoint weight loading, configures /dev/shm IPC primitives, and routes traffic dynamically based on engine metrics.

Step-by-Step Implementation

Let's build a production deployment running a model like meta-llama/Meta-Llama-3-70B-Instruct spanning 4x NVIDIA A100 (80GB) GPUs using Tensor Parallelism (TP=4).

1. Custom Triton C++/Python Integration Script (model.py)

We utilize Triton's Python Backend to wrap vLLM's AsyncLLMEngine. This enables Triton to consume decoupled responses as tokens stream off the CUDA stream.

# model.py - Triton Python Backend wrapping vLLM Engine
import json
import asyncio
import triton_python_backend_utils as pb_utils
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.engine.async_llm_engine import AsyncLLMEngine
from vllm.sampling_params import SamplingParams

class TritonPythonModel:
    def initialize(self, args):
        self.model_config = json.loads(args["model_config"])
        
        # Parse model parameters passed via Triton's config.pbtxt
        parameters = self.model_config.get("parameters", {})
        model_path = parameters.get("model_path", {}).get("string_value", "/models/llama-3-70b")
        tensor_parallel_size = int(parameters.get("tensor_parallel_size", {}).get("string_value", "4"))
        gpu_memory_utilization = float(parameters.get("gpu_memory_utilization", {}).get("string_value", "0.90"))
        max_num_seqs = int(parameters.get("max_num_seqs", {}).get("string_value", "256"))

        # Configure vLLM Async Engine
        engine_args = AsyncEngineArgs(
            model=model_path,
            tensor_parallel_size=tensor_parallel_size,
            gpu_memory_utilization=gpu_memory_utilization,
            max_num_seqs=max_num_seqs,
            enable_chunked_prefill=True,  # Critical for TTFT vs TPOT smoothing
            trust_remote_code=True,
            enforce_eager=False # Enable CUDA graphs for fast execution
        )
        
        # Initialize Event Loop for Triton Async Execution
        self.loop = asyncio.get_event_loop()
        self.engine = AsyncLLMEngine.from_engine_args(engine_args)

    def execute(self, requests):
        """Processes batches incoming from Triton's dynamic scheduler."""
        responses = []
        for request in requests:
            # Create a background task for each decoupled dynamic response stream
            response_sender = request.get_response_sender()
            self.loop.create_task(self._process_request(request, response_sender))
        return None

    async def _process_request(self, request, response_sender):
        try:
            prompt = pb_utils.get_input_tensor_by_name(request, "PROMPT").as_numpy()[0].decode("utf-8")
            max_tokens = int(pb_utils.get_input_tensor_by_name(request, "MAX_TOKENS").as_numpy()[0])
            temperature = float(pb_utils.get_input_tensor_by_name(request, "TEMPERATURE").as_numpy()[0])

            sampling_params = SamplingParams(
                max_tokens=max_tokens,
                temperature=temperature,
            )

            request_id = str(id(request))
            results_generator = self.engine.generate(prompt, sampling_params, request_id)

            async for request_output in results_generator:
                # Streaming out back to client through Triton Decoupled Interface
                output_text = request_output.outputs[0].text
                output_tensor = pb_utils.Tensor("TEXT_OUTPUT", numpy.array([output_text.encode("utf-8")], dtype=object))
                
                response = pb_utils.InferenceResponse(output_tensors=[output_tensor])
                response_sender.send(response, flags=pb_utils.TRITONSERVER_RESPONSE_COMPLETE_FINAL 
                                     if request_output.finished else 0)

        except Exception as e:
            error_tensor = pb_utils.Tensor("ERROR", numpy.array([str(e).encode("utf-8")], dtype=object))
            response_sender.send(pb_utils.InferenceResponse(output_tensors=[error_tensor]), 
                                 flags=pb_utils.TRITONSERVER_RESPONSE_COMPLETE_FINAL)

    def finalize(self):
        pass

2. Triton Configuration (config.pbtxt)

We configure Triton for decoupled response patterns (allowing streaming) and specify engine runtime arguments:

name: "vllm_llama3_70b"
backend: "python"
max_batch_size: 0  # vLLM internal engine controls continuous batching

model_transaction_policy {
  decoupled: true
}

input [
  {
    name: "PROMPT"
    data_type: TYPE_STRING
    dims: [ 1 ]
  },
  {
    name: "MAX_TOKENS"
    data_type: TYPE_INT32
    dims: [ 1 ]
  },
  {
    name: "TEMPERATURE"
    data_type: TYPE_FP32
    dims: [ 1 ]
  }
]

output [
  {
    name: "TEXT_OUTPUT"
    data_type: TYPE_STRING
    dims: [ 1 ]
  }
]

parameters: {
  key: "model_path"
  value: { string_value: "/mnt/models/Meta-Llama-3-70B-Instruct" }
}
parameters: {
  key: "tensor_parallel_size"
  value: { string_value: "4" }
}
parameters: {
  key: "gpu_memory_utilization"
  value: { string_value: "0.92" }
}
parameters: {
  key: "max_num_seqs"
  value: { string_value: "512" }
}

instance_group [
  {
    count: 1
    kind: KIND_GPU
    gpus: [ 0, 1, 2, 3 ]
  }
]

3. Kubernetes Deployment Topology

Deploying high-performance GPU workloads requires careful physical host resource binding. We explicitly map shared memory (/dev/shm) for NCCL inter-process communication and use local NVMe SSD host paths for ultra-fast weight loading.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: triton-vllm-llama3
  namespace: llm-serving
  labels:
    app: triton-vllm-llama3
spec:
  replicas: 2
  selector:
    matchLabels:
      app: triton-vllm-llama3
  template:
    metadata:
      labels:
        app: triton-vllm-llama3
    spec:
      # Direct pod mapping to identical NUMA / NVLink GPU Topologies
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: accelerator
                operator: In
                values:
                - nvidia-a100-sxm4-80gb
      containers:
      - name: triton-inference-server
        image: nvcr.io/nvidia/tritonserver:24.03-py3
        command: ["tritonserver"]
        args:
          - "--model-repository=/models/triton_repo"
          - "--grpc-port=8001"
          - "--http-port=8000"
          - "--metrics-port=8002"
          - "--log-verbose=1"
        resources:
          limits:
            nvidia.com/gpu: "4" # Allocate 4 contiguous GPUs on the single node
            memory: "250Gi"
            cpu: "32"
          requests:
            nvidia.com/gpu: "4"
            memory: "180Gi"
            cpu: "16"
        ports:
          - containerPort: 8000
            name: http
          - containerPort: 8001
            name: grpc
          - containerPort: 8002
            name: metrics
        volumeMounts:
          - mountPath: /dev/shm
            name: dshm
          - mountPath: /models/triton_repo
            name: model-repo
          - mountPath: /mnt/models
            name: model-weights
        env:
          - name: NCCL_DEBUG
            value: "INFO"
          - name: CUDA_DEVICE_MAX_CONNECTIONS
            value: "1"
      volumes:
        # Essential to avoid PyTorch/NCCL Out-of-Memory Shared Execution Failures
        - name: dshm
          emptyDir:
            medium: Memory
            sizeLimit: 32Gi
        - name: model-repo
          configMap:
            name: triton-model-repo-config
        # Rapid loading from cached NVMe nodes instead of downloading at start
        - name: model-weights
          hostPath:
            path: /mnt/nvme/models
            type: Directory

Latency Optimization Benchmarks & Parameter Tuning

To achieve that 40% latency reduction, standard configs aren't enough. You must tune vLLM and Triton to match your specific hardware topology.

+---------------------------------------------------------------------------------------+
| Tuning Parameter             | Baseline Setting | Optimized Value | Latency / Memory Impact|
+---------------------------------------------------------------------------------------+
| gpu_memory_utilization       | 0.80             | 0.92 - 0.95     | Increases max context  |
|                              |                  |                 | cache capacity by ~18% |
+---------------------------------------------------------------------------------------+
| enable_chunked_prefill       | False            | True            | Prevents TTFT spikes;  |
|                              |                  |                 | drops P99 TPOT by ~35% |
+---------------------------------------------------------------------------------------+
| max_num_batched_tokens       | 2048             | 5120            | Drastically increases  |
|                              |                  |                 | multi-prompt prefill   |
+---------------------------------------------------------------------------------------+
| Shared Memory (/dev/shm)     | Default 64MB     | 32GB+           | Prevents IPC inter-GPU |
|                              |                  |                 | communication hangs   |
+---------------------------------------------------------------------------------------+

Decoupling TTFT and TPOT with Chunked Prefill

Without chunked prefills, a high-token prompt (e.g., 4,000 tokens) forces the engine to process the entire prefill in a single compute-heavy step. This starves active decode streams, causing extreme P99 TPOT jitter.

By setting enable_chunked_prefill=True, vLLM chunks large prefills into smaller subsets (e.g., 512 tokens per iteration) and co-schedules them alongside decoding streams. This maintains smooth token-by-token execution for existing users while processing new requests.

Without Chunked Prefill:
Time -->
Step 1: [ Large Prefill (4000 tokens) - All Decode Steps Blocked... ] ❌ Latency Spike
Step 2: [ Decode Req A ][ Decode Req B ]

With Chunked Prefill:
Time -->
Step 1: [ Prefill Chunk (512 t) ][ Decode Req A ][ Decode Req B ] ✅ Smooth P99 Stream
Step 2: [ Prefill Chunk (512 t) ][ Decode Req A ][ Decode Req B ]

Production Observability and Autoscaling with KEDA

Standard Kubernetes Autoscalers (HPA) scale workloads based on CPU or generic GPU compute utilization metrics (container_gpu_utilization). For LLMs, this is broken: GPU compute is almost always near 100% because CUDA kernels burn cycles waiting for memory bandwidth during decoding operations, even if the request queue is empty.

To scale efficiently, you must autoscale based on KV Cache saturation and Waiting Queue Length.

Prometheus Metrics Extracted from vLLM/Triton

  • vllm:num_requests_waiting: Requests sitting idle in memory queue (Primary scaling signal).
  • vllm:gpu_cache_usage_perc: Real-time physical allocation percentage of virtual KV blocks.

KEDA ScaledObject Configuration

Below is a production-grade KEDA manifest configured to scale pods out before the KV cache saturates or queue lengths degrade latency:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: triton-vllm-autoscaler
  namespace: llm-serving
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: triton-vllm-llama3
  minReplicaCount: 2
  maxReplicaCount: 10
  cooldownPeriod: 300
  pollingInterval: 15
  advanced:
    horizontalPodAutoscalerConfig:
      behavior:
        scaleUp:
          stabilizationWindowSeconds: 0
          policies:
          - type: Percent
            value: 100
            periodSeconds: 15
        scaleDown:
          stabilizationWindowSeconds: 600 # Slow scale down to protect KV Cache churn
  triggers:
  - type: prometheus
    metadata:
      serverAddress: http://prometheus-k8s.monitoring.svc.cluster.local:9090
      metricName: vllm_num_requests_waiting
      query: |
        sum(vllm:num_requests_waiting{namespace="llm-serving", app="triton-vllm-llama3"})
      threshold: '5' # Scale out immediately if waiting queue holds more than 5 requests
  - type: prometheus
    metadata:
      serverAddress: http://prometheus-k8s.monitoring.svc.cluster.local:9090
      metricName: vllm_gpu_cache_usage_perc
      query: |
        max(vllm:gpu_cache_usage_perc{namespace="llm-serving", app="triton-vllm-llama3"})
      threshold: '0.85' # Scale out when KV cache hits 85% block capacity

Real-World Impact: Latency and Cost Reductions

By replacing a standard naive HuggingFace pipeline deployment with this architecture, we achieved substantial real-world performance improvements on a benchmark suite of 100 concurrent streams serving LLaMA-3-70B:

  • P99 TTFT (Time-To-First-Token): Reduced from 2.4 seconds to 820ms (~65% improvement).
  • P99 TPOT (Time-Per-Output-Token): Reduced from 110ms to 42ms (~61% improvement).
  • Overall Request Throughput: Increased by 2.8x per GPU cluster node.
  • Cloud Infrastructure Spend: Reduced total active GPU node count by 35% while handling the same peak SLA requirement, yielding significant monthly cost savings on AWS (p4d.24xlarge instances).

Conclusion

Scaling LLMs in enterprise environments requires moving beyond basic single-instance frameworks. Wrapping vLLM's PagedAttention engine inside Triton Inference Server on Kubernetes provides the infrastructure rigor needed for mission-critical deployments.

By taking control of virtual memory page mapping, decoupling network streaming, using smart chunked prefill scheduling, and autoscaling on actual KV cache load metrics, you eliminate memory starvation bottlenecks. This results in ultra-low tail latencies, maximum hardware efficiency, and vastly reduced cloud compute bills.