The Practical Playbook for AI Model Serving Latency Optimization
Practical techniques to cut AI model serving latency: from quantization and batching to network, GPU, and scheduler tuning, with checklists and examples.
Image used for representation purposes only.
Why Latency Is the North Star of Model Serving
Low latency is the difference between a product that feels magical and one that feels sluggish. In AI serving, latency translates directly into user satisfaction, conversion, and infrastructure cost. The challenge: latency is an end-to-end property shaped by networks, queues, kernels, and model architecture—not just “the model.”
This article provides a practical playbook to reduce p95/p99 latency for both classic ML and LLM workloads, including concrete tactics, code snippets, and a 10-step tuning blueprint.
Define the Latency Budget
Start by writing an explicit budget. For online inference, a simple model is:
- T_total = T_dns + T_tls + T_conn + T_queue + T_prefill + T_decode + T_post + T_egress
- For LLMs, also track:
- Time-To-First-Token (TTFT): dominated by prefill and scheduling
- Inter-Token Latency (ITL): dominated by decode throughput and KV cache efficiency
- Tokens Per Second (TPS) and concurrency (active requests per GPU)
Example SLOs:
- Small model APIs: p95 < 150 ms per request
- Streaming LLM chat: TTFT < 200 ms, ITL < 30 ms/token at p95 under target concurrency
Quantify each component with tracing before any optimizations.
Understand Your Workload
Latency tactics depend on workload shape:
- Single-shot classification/regression: typically batch-friendly, GPU/CPU both viable
- Embedding/feature extraction: highly batchable; cache aggressively
- Generative LLMs: two phases
- Prefill (bandwidth/compute heavy, benefits from batching)
- Decode (memory/latency sensitive; batching helps but risks head-of-line blocking)
Model-Level Optimizations (Biggest Wins First)
- Right-size the model
- Use distillation or smaller architectures when acceptable.
- Consider multi-quantized tiers (e.g., INT4/INT8 for interactive tasks, BF16/FP8 for premium quality).
- Quantization
- Weight-only INT8/INT4 or FP8 reduces memory bandwidth and improves cache residency.
- Calibrate with representative data; validate p95 accuracy impact not just mean.
- For LLMs, combine weight quantization with KV-cache quantization (e.g., INT8/FP8) for decode gains.
- Graph and kernel fusion
- Export to ONNX and apply runtime accelerators (e.g., TensorRT, ONNX Runtime, OpenVINO, BetterTransformer-like fusions).
- Prefer fused attention kernels (e.g., FlashAttention, memory-efficient attention).
- Attention architecture tweaks (LLMs)
- Multi-Query or Grouped-Query Attention to cut KV size and memory traffic.
- Paged attention/KV paging to scale concurrency without OOM.
- Speculative decoding (draft + verifier) to reduce TTFT/ITL when quality allows.
- Prompt and KV caching (LLMs)
- Prompt cache: hash normalized prompts; reuse prefill results.
- Continuation reuse: persist KV cache across turns for the same session.
System-Level Optimizations
- Dynamic batching and microbatching
- Dynamic batching improves throughput but can harm tail latency. Use deadlines and maximum queue delay.
- For LLM decode, prefer continuous batching (append-only token scheduling) to keep SMs busy without blocking the world.
- Concurrency shaping
- Run multiple model instances per GPU to reduce head-of-line blocking when requests are spiky.
- Consider GPU partitioning (e.g., MIG) for strict isolation at a small efficiency cost.
- Memory and transfer
- Pin host memory and overlap H2D/D2H copies with compute using multiple CUDA streams.
- Keep hot tensors resident on device; avoid frequent model swaps.
- Favor NVLink or PCIe Gen4/5 paths; minimize cross-socket NUMA hops on multi-CPU hosts.
- Pre/postprocessing off the hot path
- Tokenize with native libraries (Rust/Go/C++), not Python loops.
- Vectorize common transforms; cache tokenized prompts when possible.
- Runtime choice matters
- Prefer high-performance inference servers with built-in schedulers, dynamic batching, and streaming I/O.
- For Python frontends, use async I/O, uvloop, and avoid holding the GIL in hot paths; move heavy work to native extensions.
Networking and Protocol Tuning
- Prefer HTTP/2 or gRPC for multiplexed streams and header compression; enable keep-alives and connection pooling.
- Use TLS session resumption and OCSP stapling; minimize handshake cost.
- Apply request/response compression only when payloads are large and CPU budget allows.
- Disable Nagle’s algorithm (TCP_NODELAY) for streaming tokens; ensure small writes coalesce reasonably in user space.
- Co-locate gateways and inference pods in the same zone; minimize cross-AZ hops.
Scheduling, Queuing, and Tail Control
- SLO-aware scheduling: assign per-request deadlines; use Earliest Deadline First (EDF) or Shortest Remaining Processing Time (SRPT) approximations where feasible.
- Admission control: reject or shed early when queues exceed SLO-derived thresholds.
- Priority classes: isolate latency-sensitive traffic from best-effort jobs.
- Hedged requests: duplicate stragglers after a percentile-based delay; cancel losers.
- Circuit breakers and timeouts: protect upstreams and prevent cascading failure.
Caching Strategies Beyond KV
- Embedding cache: deduplicate semantically identical inputs via hashing/fingerprints.
- Feature cache: cache pre/post transforms for popular payloads.
- Response cache: safe for deterministic idempotent models; bypass model entirely for cache hits.
Autoscaling and Cold Starts
- Scale on queue depth + GPU utilization + TTFT/ITL, not just CPU usage.
- Maintain warm pools; pre-load models and compile graphs ahead of traffic spikes.
- Snapshot/serialize model weights once per rollout; avoid pull-time decompression on the critical path.
Observability: Measure What You Can Improve
Instrument at four layers:
- Edge/gateway: handshake, request size, backpressure
- Service: queue time, service time, per-stage spans (prefill, decode)
- Runtime/HW: GPU SM occupancy, memory BW, H2D/D2H throughput, kernel duration heatmaps
- Application: TTFT, ITL, TPS, p50/p95/p99, error classes
Adopt RED (Rate, Errors, Duration) plus high-cardinality traces to isolate variance. Use percentile histograms and heatmaps; avoid averages.
Benchmarking the Right Way
- Use closed-loop load (e.g., fixed concurrency) to emulate user sessions; use open-loop (constant arrival rate) to study overload behavior.
- Avoid coordinated omission: tools that control for it (or custom harnesses) yield more honest tail latencies.
- Prime caches and JITs before official runs; report cold vs warm numbers.
- For LLMs, benchmark both TTFT and sustained TPS under realistic context lengths.
Practical Snippets
Dynamic batching with deadlines (conceptual Python):
from time import time
MAX_BATCH = 16
MAX_WAIT_MS = 6
queue = []
def enqueue(req):
req.deadline = time() + (MAX_WAIT_MS / 1000)
queue.append(req)
def dispatch():
now = time()
# Prefer requests closest to their deadlines
queue.sort(key=lambda r: r.deadline)
batch, remain = queue[:MAX_BATCH], queue[MAX_BATCH:]
# If earliest deadline is near, send even a small batch
if batch and (batch[0].deadline <= now or len(batch) == MAX_BATCH):
send_to_gpu(batch)
else:
queue[:] = queue # wait a bit more
FastAPI streaming with TCP_NODELAY (conceptual):
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import socket
app = FastAPI()
@app.get('/generate')
async def generate(q: str):
async def token_stream():
for tok in model.stream(q):
yield tok
resp = StreamingResponse(token_stream(), media_type='text/plain')
# Ensure TCP_NODELAY via server/socket config (framework-specific)
return resp
Tokenizer offload example (batching inputs):
# Avoid per-request tokenizer startup; batch and reuse
batched_texts = collect_texts(n=128)
ids = fast_native_tokenizer.encode_batch(batched_texts)
A 10-Step Latency Tuning Blueprint
- Map the path: trace requests and write a latency budget (TTFT/ITL for LLMs).
- Set SLOs and guardrails: p95 targets, max queueing delay, admission rules.
- Remove Python from hotspots: native tokenizers, fused ops, kernel accelerators.
- Quantize and fuse: INT4/8 or FP8 weights; fused attention; export and compile graphs.
- Right-size batching: dynamic microbatches with deadlines; continuous batching for decode.
- Shape concurrency: multiple instances per GPU; isolate tenants with MIG or cgroups.
- Optimize memory and transfer: pinned memory, overlapping copies, avoid model swaps.
- Harden the network: HTTP/2 or gRPC, keep-alives, TLS resumption, TCP_NODELAY.
- Control tails: SLO-aware scheduling, hedged requests, circuit breakers, early shedding.
- Instrument and iterate: percentile histograms, kernel-level profiling, A/B rollouts.
Common Pitfalls to Avoid
- Chasing throughput at the cost of p95: oversized batches or long queueing windows.
- Tokenizing in the request thread with pure Python.
- Ignoring decode-time memory bandwidth for LLMs (ITL explodes under high concurrency).
- Underestimating TLS/handshake costs at the edge.
- Missing warmups and JIT compilation in benchmarks.
Final Thoughts
End-to-end latency is a systems problem. The fastest path emerges from many small wins: a lean model, fused kernels, smart batching, disciplined networking, and SLO-aware scheduling—measured relentlessly with good telemetry. Treat the latency budget as living documentation, revisit it with each model or traffic change, and keep your p95 honest with rigorous benchmarking.
Related Posts
Prompt Chaining: Designing Reliable Multi‑Step AI Workflows
A practical guide to building robust multi-step AI pipelines with prompt chaining, orchestration patterns, state, testing, guardrails, and cost control.
API Orchestration vs. Choreography: Making the Right Integration Choice
Understand orchestration vs choreography for APIs: definitions, trade-offs, patterns, tools, and a practical decision guide.
Building an AI Customer Churn Prediction API: Architecture, Endpoints, and Best Practices
A practical guide to designing an AI churn prediction API—architecture, data/modeling choices, endpoints, MLOps, metrics, and code examples.