Skip to content
Posts en inglés. Usá el traductor del navegador para leerlos en tu idioma.

How to Optimize LLM Inference with Continuous Batching for Better Latency

Yammbo
· 6 min read
llm inference latency vllm optimization p99 latency tail latency chunked prefill
How to Optimize LLM Inference with Continuous Batching for Better Latency

Large Language Model (LLM) serving systems face a critical challenge: balancing high throughput with low latency. While continuous batching is a widely adopted optimization that significantly boosts overall efficiency and median (p50) latency, its impact on tail latency (p99) is often more nuanced and less understood. This tutorial guides you through the intricacies of continuous batching, chunked prefill, and preemption strategies within an LLM serving framework like vLLM, helping you configure your setup to achieve optimal performance for real-world production environments.

Understanding Batching Strategies in LLM Inference

Before diving into configurations, it's essential to grasp the fundamental differences between batching strategies in LLM inference. These strategies dictate how incoming user requests are grouped and processed by the GPU.

Static Batching

In a static batching setup, incoming requests are held in a queue until a predefined batch size is met, or a timeout occurs. Once a full batch is assembled, all requests within it are processed simultaneously. This approach can be simple to manage and ensures high GPU utilization when a constant stream of requests is available. However, under fluctuating or heavy load, individual requests might experience significant delays waiting for a batch to fill, leading to high admission latency and unpredictable performance, especially for interactive applications.

Continuous Batching

Continuous batching, on the other hand, admits requests almost instantly. Instead of waiting for a full batch, the system dynamically manages a pool of in-flight requests, scheduling them to run on the GPU as resources become available. This approach keeps the GPU busy more consistently, dramatically improving overall throughput and often reducing median (p50) latency because requests don't idle in an admission queue. Modern LLM serving engines, including vLLM, leverage continuous batching as a core feature.

While continuous batching offers substantial benefits, it introduces a different set of challenges. By constantly juggling multiple requests, the system can introduce occasional pauses or 'jitter' during the token streaming process for individual requests. This dynamic resource allocation can sometimes lead to increased tail latency (p99), where a small percentage of requests experience much longer response times than the median.

Setting Up Your LLM Inference Environment with vLLM

To practically explore the effects of continuous batching and its configurations, we'll set up a basic LLM serving environment using vLLM. vLLM is an open-source library designed for fast LLM inference and offers excellent control over the parameters we'll be discussing.

Prerequisites

  • Python: Ensure you have Python 3.8 or newer installed.
  • pip: Python's package installer.
  • GPU: An NVIDIA GPU with CUDA support (version 11.8 or 12.1 recommended for vLLM compatibility).

Installation

First, install vLLM using pip. It's often a good practice to do this within a virtual environment.

pip install vllm

Running a Basic vLLM Server

Once installed, you can start a vLLM API server with a common open-source LLM. For this tutorial, we'll use a widely available model like Llama 2 7B. You can find details about the Llama 2 7B model on Hugging Face.

python -m vllm.entrypoints.api_server --model meta-llama/Llama-2-7b-hf --port 8000

This command starts a server on http://localhost:8000, loading the Llama 2 7B model. The --model argument specifies the model identifier from the Hugging Face Model Hub. For more details on vLLM installation and usage, refer to the official vLLM documentation.

Verification

After running the command, you should see logs indicating the model loading and the server starting. Look for a message similar to "Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)". You can test it by sending a simple request using curl or a Python script.

Configuring vLLM for Batching and Prefill Management

vLLM's default settings are optimized for high throughput using continuous batching. However, to understand and mitigate tail latency issues, we need to explore specific configuration options related to prefill management and preemption.

Chunked Prefill

The initial processing of a user's prompt (the 'prefill' phase) can be computationally intensive, especially for long prompts. In a continuous batching system, a long prefill operation for one request can momentarily stall the token generation (decode) for other in-flight requests, contributing to tail latency.

Chunked prefill addresses this by breaking down the prefill phase into smaller, manageable chunks. This allows the system to interleave prefill computations with decode operations from other requests, reducing the likelihood of a single long prefill blocking the entire GPU for an extended period. Modern vLLM versions (v1.0 and later) enable chunked prefill by default, often coupled with a decode-first scheduling policy.

While typically enabled by default, you can explicitly control it if needed (though it's generally recommended to keep it on for better tail latency):

python -m vllm.entrypoints.api_server --model meta-llama/Llama-2-7b-hf --enable-chunked-prefill

By default, vLLM's internal scheduling prioritizes decoding tokens for already-running requests over processing new prompt prefills, further mitigating stalls.

Preemption Strategy

Another critical factor influencing tail latency is how the LLM serving engine handles KV (Key-Value) cache pressure. The KV cache stores intermediate activations during token generation, consuming significant GPU memory. When the GPU's memory becomes full, some requests might need to be 'preempted' to free up space.

vLLM offers different preemption modes, which significantly impact tail latency:

  1. RECOMPUTE (Default): When a request is preempted under KV cache pressure, its entire prefill and subsequent token generation up to the point of preemption must be recomputed later when resources become available. This approach saves memory by not storing the evicted KV cache, but it can dramatically increase the latency for the affected request, leading to spikes in p99.
  2. SWAP: In this mode, when a request is preempted, its KV cache is swapped from GPU memory to CPU memory. When the request is resumed, its KV cache is swapped back to the GPU. This avoids recomputation, leading to potentially smoother tail latency. However, it requires more system memory (RAM) to store the swapped KV caches and incurs latency overhead for the memory transfer between GPU and CPU.

You can configure the preemption mode using the --scheduler-preemption-mode argument:

# To use RECOMPUTE (default behavior, potentially higher p99)python -m vllm.entrypoints.api_server --model meta-llama/Llama-2-7b-hf --scheduler-preemption-mode RECOMPUTE# To use SWAP (potentially smoother p99, but higher RAM usage)python -m vllm.entrypoints.api_server --model meta-llama/Llama-2-7b-hf --scheduler-preemption-mode SWAP

The choice between RECOMPUTE and SWAP depends on your specific hardware constraints (available GPU memory vs. system RAM) and your tolerance for tail latency spikes.

Simulating Workloads and Collecting Latency Data

To observe the effects of these configurations, you need to simulate a realistic workload and measure the resulting latencies. A 'mixed-length traffic trace'—a series of requests with varying prompt and generation lengths—is crucial for uncovering tail latency issues that might not appear with uniform, short requests.

Creating a Simple Client Script

You can use a Python script with the httpx library (or requests) to send concurrent requests to your vLLM server and measure response times. This example demonstrates how to send a request and calculate Time To First Token (TTFT) and Time To Last Token (TTLT).

import httpximport asyncioimport timeimport statisticsasync def send_request(prompt: str, max_tokens: int, url: str):    start_time = time.time()    ttft = None    full_response = ""    try:        async with httpx.AsyncClient(timeout=60) as client:            async with client.stream("POST", url, json={                "prompt": prompt,                "max_tokens": max_tokens,                "stream": True            }) as response:                response.raise_for_status()                async for chunk in response.aiter_bytes():                    if ttft is None:                        ttft = time.time() - start_time                    full_response += chunk.decode('utf-8', errors='ignore')        ttlt = time.time() - start_time        return {"ttft": ttft, "ttlt": ttlt, "success": True}    except httpx.HTTPStatusError as e:        print(f"HTTP error occurred: {e.response.status_code} - {e.response.text}")        return {"success": False}    except httpx.RequestError as e:        print(f"An error occurred while requesting {e.request.url!r}: {e}")        return {"success": False}async def main():    server_url = "http://localhost:8000/generate"    prompts = [        ("Write a very short poem about AI:", 50),        ("Explain the concept of quantum entanglement in simple terms:", 200),        ("Tell a story about a space explorer discovering a new planet, focusing on its unique flora and fauna:", 500),        ("What is the capital of France?", 10),        ("Describe the process of photosynthesis.", 150)    ]    num_concurrent_requests = 10    all_ttfts = []    all_ttlts = []    tasks = []    for _ in range num_concurrent_requests:        for prompt_text, max_tok in prompts:            tasks.append(send_request(prompt_text, max_tok, server_url))    results = await asyncio.gather(*tasks)    for res in results:        if res["success"]:            all_ttfts.append(res["ttft"])            all_ttlts.append(res["ttlt"])    if all_ttfts:        print(f"\n--- Latency Statistics ({len(all_ttfts)} successful requests) ---")        print(f"TTFT (Time To First Token) - Median (p50): {statistics.median(all_ttfts):.4f}s")        print(f"TTFT (Time To First Token) - 99th Percentile (p99): {statistics.quantiles(all_ttfts, n=100)[98]:.4f}s")        print(f"TTLT (Time To Last Token) - Median (p50): {statistics.median(all_ttlts):.4f}s")        print(f"TTLT (Time To Last Token) - 99th Percentile (p99): {statistics.quantiles(all_ttlts, n=100)[98]:.4f}s")    else:        print("No successful requests to report statistics.")if __name__ == "__main__":    asyncio.run(main())

Running the Simulation and Collecting Data

Save the above code as latency_client.py. Make sure your vLLM server is running, then execute the client script:

python latency_client.py

Run this client script multiple times with different vLLM server configurations (e.g., with and without chunked prefill, and with different preemption modes). Collect the reported p50 and p99 TTFT and TTLT values for each configuration.

Verification

The client script will print median (p50) and 99th percentile (p99) latency values for both Time To First Token (TTFT) and Time To Last Token (TTLT). These are the metrics you'll use for analysis.

Analyzing Latency Tradeoffs and Optimization Strategies

Once you've collected latency data across different vLLM configurations, you can analyze the tradeoffs and identify optimal strategies for your specific use case.

Interpreting Continuous Batching's Impact

You'll likely observe that continuous batching, by default, provides excellent overall throughput and a low p50 TTLT. This confirms its efficiency in keeping the GPU utilized and quickly serving the majority of requests.

The Role of Chunked Prefill

When comparing results with and without chunked prefill (if you were to disable it for testing, which is not recommended for production), you would typically see that enabling chunked prefill significantly reduces p99 TTFT and TTLT, especially for workloads with varying prompt lengths. This is because it prevents long initial prompt processing from monopolizing GPU resources and stalling other in-flight requests' decode phases.

Evaluating Preemption Modes

This is where the most pronounced differences in tail latency often appear:

  • RECOMPUTE (Default): You might notice higher p99 TTLT values, particularly under heavy load or when memory pressure is high. This is a direct consequence of requests being fully recomputed after preemption, adding significant delays for those affected requests. While efficient in terms of GPU memory, it sacrifices tail latency.
  • SWAP: Switching to SWAP mode often results in a smoother p99 TTLT. The overhead of swapping KV caches to CPU memory is generally less impactful than full recomputation, leading to more predictable tail latencies. However, monitor your system's RAM usage, as SWAP mode can consume considerably more host memory.

Balancing Performance Metrics

Optimizing LLM inference is a balancing act:

  • If your primary concern is overall throughput and median latency, vLLM's default continuous batching with chunked prefill is often sufficient.
  • If low tail latency (p99) is critical for user experience in interactive applications, consider adjusting the preemption mode to SWAP, provided you have ample system RAM.
  • Always consider your specific workload: the distribution of prompt lengths, generation lengths, and concurrency levels will dictate which optimizations yield the most benefit.

Experimentation with real-world traffic patterns and careful monitoring of both p50 and p99 metrics are key to finding the sweet spot for your LLM serving infrastructure.

Mastering LLM inference performance requires a deep understanding of underlying mechanisms like continuous batching, prefill management, and preemption strategies. By experimenting with configurations in tools like vLLM and analyzing the resulting latency profiles, you can effectively balance throughput and tail latency to meet the demands of your specific applications. For more insights into building high-performance web applications, explore the capabilities of Yammbo Web at https://web.yammbo.com.