14 — LLM Inference Observability — Special Topic¶
LLM serving has its own metric vocabulary, its own bottlenecks, and its own SLO menu. Generic inference observability (doc 04) gets you 60% of the way; this doc covers the other 40% — the metrics that vLLM, TensorRT-LLM, and TGI emit that no other workload class does.
The reference is vLLM's metric surface (the most-instrumented LLM serving framework as of 2026). TGI and Triton+TensorRT-LLM expose subsets with similar semantics.
1. Why LLM Inference Is Different¶
Three things make LLM inference uniquely hard:
| Property | Implication |
|---|---|
| Two distinct phases (prefill, decode) | Different SLOs apply to each |
| State (KV cache) that persists across tokens | Memory pressure, not compute, often dominates |
| Variable output length | Tail latency is workload-driven, not infra-driven |
A request takes the prefill phase (process input, compute KV cache), then the decode phase (generate one token at a time). Prefill is compute-bound (one big GEMM); decode is memory-bound (small GEMMs reading the entire KV cache each step). Optimization, observation, and SLOs differ.
2. The vLLM Metric Surface¶
vLLM exposes ~60 Prometheus metrics on :8000/metrics. The ones that matter for production:
2.1 Latency¶
| Metric | Type | What |
|---|---|---|
vllm:time_to_first_token_seconds |
histogram | TTFT — time from request received to first output token |
vllm:inter_token_latency_seconds |
histogram | ITL — gap between consecutive output tokens |
vllm:e2e_request_latency_seconds |
histogram | Full request duration |
vllm:request_queue_time_seconds |
histogram | Time spent waiting before scheduling |
vllm:request_prefill_time_seconds |
histogram | Prefill phase duration |
vllm:request_decode_time_seconds |
histogram | Decode phase duration |
vllm:request_inference_time_seconds |
histogram | Total RUNNING phase duration |
2.2 Tokens / throughput¶
| Metric | Type | What |
|---|---|---|
vllm:generation_tokens |
counter | Total output tokens generated |
vllm:prompt_tokens |
counter | Total input (prefill) tokens |
vllm:prompt_tokens_cached |
counter | Cached prompt tokens (didn't re-prefill) |
vllm:iteration_tokens_total |
histogram | Tokens per engine step (decode batch size, effectively) |
2.3 Cache (the heart of vLLM)¶
| Metric | Type | What |
|---|---|---|
vllm:kv_cache_usage_perc |
gauge | Fraction of KV cache blocks in use |
vllm:prefix_cache_queries |
counter | Total cache lookups (per token) |
vllm:prefix_cache_hits |
counter | Total cache hits |
vllm:external_prefix_cache_queries |
counter | Cross-instance KV cache (KV connector) |
vllm:external_prefix_cache_hits |
counter | Cross-instance hits |
vllm:kv_block_lifetime_seconds |
histogram | How long blocks live before eviction |
vllm:kv_block_idle_before_evict_seconds |
histogram | How long evicted blocks were idle |
vllm:kv_block_reuse_gap_seconds |
histogram | Time between same block re-uses |
2.4 Scheduling & batching¶
| Metric | Type | What |
|---|---|---|
vllm:num_requests_running |
gauge | Active requests in the current batch |
vllm:num_requests_waiting |
gauge | Queue depth |
vllm:num_requests_waiting_by_reason |
gauge | Labeled by capacity or deferred |
vllm:num_preemptions |
counter | KV evictions causing request preemption |
2.5 Speculative decoding¶
| Metric | Type | What |
|---|---|---|
vllm:spec_decode_num_drafts |
counter | Draft inference invocations |
vllm:spec_decode_num_draft_tokens |
counter | Tokens proposed by draft model |
vllm:spec_decode_num_accepted_tokens |
counter | Tokens accepted by target |
vllm:spec_decode_num_accepted_tokens_per_pos |
counter | By position in draft |
2.6 LoRA¶
| Metric | Type | What |
|---|---|---|
vllm:lora_requests_info |
gauge | Per-adapter active request stats |
2.7 MFU (Model FLOPs Utilization)¶
| Metric | Type | What |
|---|---|---|
vllm:estimated_flops_per_gpu_total |
counter | Estimated FLOPs done |
vllm:estimated_read_bytes_per_gpu_total |
counter | HBM bytes read |
vllm:estimated_write_bytes_per_gpu_total |
counter | HBM bytes written |
3. The Six Dashboard Panels Every LLM Service Needs¶
3.1 TTFT and ITL percentiles¶
histogram_quantile(0.5, sum by (le) (rate(vllm:time_to_first_token_seconds_bucket[5m])))
histogram_quantile(0.95, ...)
histogram_quantile(0.99, ...)
histogram_quantile(0.5, sum by (le) (rate(vllm:inter_token_latency_seconds_bucket[5m])))
histogram_quantile(0.95, ...)
histogram_quantile(0.99, ...)
These are the user-facing SLOs. For a chat assistant, TTFT < 200ms, ITL < 50ms is typical.
3.2 Token throughput¶
# Generation tokens per second
rate(vllm:generation_tokens[1m])
# Prompt (prefill) tokens per second
rate(vllm:prompt_tokens[1m])
The shape difference matters: generation rate is bounded by decode (small batches, memory-bound). Prompt rate is bounded by prefill (large batches, compute-bound). They scale differently with load.
3.3 KV cache pressure¶
# Current KV usage (gauge)
max(vllm:kv_cache_usage_perc)
# Preemption rate
rate(vllm:num_preemptions[5m])
# Block lifetime distribution — short = thrashing
histogram_quantile(0.5, sum by (le) (rate(vllm:kv_block_lifetime_seconds_bucket[5m])))
Healthy: KV usage 50–80%, preemption rate near zero, block lifetime > 30s. Pathological: usage > 95%, preemptions occurring, blocks living < 5s. The fix is bigger GPU memory or smaller max_num_seqs.
3.4 Prefix cache hit rate¶
For typical chatbot workloads with system prompts: 30–60% is good. < 10% means your cache isn't helping (try increasing gpu_memory_utilization or rethinking shared prompts).
3.5 Queue health¶
# Queue depth
vllm:num_requests_waiting
# Running requests
vllm:num_requests_running
# Reason breakdown
vllm:num_requests_waiting_by_reason
If waiting_by_reason{reason="capacity"} > 0 sustained → out of KV cache. If waiting_by_reason{reason="deferred"} → scheduler decision (not a problem).
3.6 Spec decode acceptance rate¶
# Acceptance rate per position
sum by (position) (rate(vllm:spec_decode_num_accepted_tokens_per_pos[5m]))
/
sum(rate(vllm:spec_decode_num_drafts[5m]))
Expected: ~70% at position 0, decaying. If first-position acceptance < 50%, your draft model is too divergent — re-train or pick a better draft.
4. The Prefill / Decode Split — Connecting to DCGM¶
The headline insight: prefill and decode use the GPU completely differently.
| Phase | Work | DCGM signature |
|---|---|---|
| Prefill | One big matmul per layer, batch_size × seq_len wide | High SM_active, high tensor_active, low dram_active (compute-bound) |
| Decode | Tiny matmul per step but reads entire KV | Low SM_active, low tensor_active, high dram_active (memory-bound) |
Cross-correlation:
# Mean SM_active during prefill-heavy windows
DCGM_FI_PROF_SM_ACTIVE
* on(pod, namespace) (rate(vllm:prompt_tokens[1m]) > rate(vllm:generation_tokens[1m]))
# Mean DRAM_active during decode-heavy windows
DCGM_FI_PROF_DRAM_ACTIVE
* on(pod, namespace) (rate(vllm:generation_tokens[1m]) > 2 * rate(vllm:prompt_tokens[1m]))
Common diagnostic: SM_active is "low" (40%) and someone's worried — check the workload mix. If it's decode-heavy, 40% SM with 80% DRAM is correct for memory-bound decode. The fix isn't compute-side; it's memory throughput (move to H200 or bigger HBM).
5. GPU Memory Breakdown¶
vLLM doesn't directly expose model weights vs activations vs KV cache in HBM, but you can derive:
# Total HBM used by this pod's GPU
DCGM_FI_DEV_FB_USED
* on(pod, namespace) group_left() kube_pod_labels{label_app="vllm"}
# KV cache bytes (vLLM derives from kv_cache_usage_perc × allocated KV memory)
vllm:kv_cache_usage_perc
* on(pod) (vllm_kv_cache_total_bytes) # if exposed
When kv_cache_usage_perc is high but DCGM_FI_DEV_FB_USED is also high (close to 80GB on H100), the GPU is saturated end-to-end. There's no headroom; reduce max_num_seqs or scale out.
5.1 Fragmentation¶
vLLM uses PagedAttention which is the answer to KV fragmentation. The bookkeeping metric:
# Block churn: created vs evicted
rate(vllm:kv_blocks_allocated[5m])
rate(vllm:kv_blocks_evicted[5m])
If eviction rate > allocation rate sustained, you have churn (preemption-driven). If allocation >> eviction, you're growing into the cache (good). If they're equal at high rates, healthy steady-state.
6. Tensor Parallelism Health¶
For models served across multiple GPUs (--tensor-parallel-size 4), all TP ranks should have identical SM_active, DRAM_active. Asymmetry is a problem.
# Variance in SM_active across TP ranks
stddev by (pod) (DCGM_FI_PROF_SM_ACTIVE)
* on(pod, namespace) group_left() kube_pod_labels{label_app="vllm"}
> 0.05
If variance > 5%, one rank is lagging — likely NVLink or thermal issue on that GPU (drilldown via doc 06).
7. LoRA Adapter Loading¶
vLLM with LoRA serves N adapters in one batch. The metric:
Adapter loading/unloading is overhead — the goal is high reuse:
# Compute the rate of adapter switches (custom; emitted via instrumentation)
rate(vllm:lora_adapter_loads_total[5m])
If the cluster cycles through adapters rapidly (one-shot serving), the loading overhead dominates and you should pin hot adapters. The metric tells you which ones.
8. Continuous Batching Efficiency¶
vLLM's killer feature is continuous batching: requests join the running batch mid-stream. The efficiency metric:
# Tokens per engine step — proxy for batch density
histogram_quantile(0.5, sum by (le) (rate(vllm:iteration_tokens_total_bucket[5m])))
Plot p50, p95 over time. A healthy server: p50 around 100–200 tokens per step (decode batch of 50 with 2-4 tokens each via spec decoding). Low values mean the batch is sparsely filled — under-loaded server.
9. SLOs in Composite¶
The "throughput at SLO" panel in doc 09 §5.2 specialized for LLM:
# Tokens/sec while p99 TTFT < 200ms and p99 ITL < 50ms
sum(rate(vllm:generation_tokens[5m]))
unless on()
(
histogram_quantile(0.99, sum by (le) (rate(vllm:time_to_first_token_seconds_bucket[5m]))) > 0.2
or
histogram_quantile(0.99, sum by (le) (rate(vllm:inter_token_latency_seconds_bucket[5m]))) > 0.05
)
Returns a number when both SLOs are met, blank otherwise. Plot over time → "what was our effective serving capacity yesterday."
10. Multimodal Cache¶
Vision-language models cache image embeddings:
Highly workload-dependent: a chatbot rarely re-uses images (low hit rate, OK). A document-RAG system re-uses them constantly (high hit rate expected, problem if not).
11. NIXL — Cross-Instance KV Sharing¶
The newer KV-connector pattern (NIXL) shares KV blocks across vLLM instances. Observability:
# Cross-instance hit rate
rate(vllm:external_prefix_cache_hits[5m]) / rate(vllm:external_prefix_cache_queries[5m])
# Transfer time
histogram_quantile(0.95, sum by (le) (rate(vllm:nixl_xfer_time_seconds_bucket[5m])))
# Bytes transferred
rate(vllm:nixl_bytes_transferred_sum[5m])
If transfer time dominates the request latency budget, cross-instance KV sharing is hurting more than helping; tune the NIXL config.
12. Alerts Specific to LLM Serving¶
Beyond the generic inference alerts in doc 10:
- alert: vLLM_KVCache_Saturated
expr: max(vllm:kv_cache_usage_perc) > 0.95
for: 5m
labels: { severity: warning }
annotations:
summary: "KV cache > 95% — preemption imminent"
- alert: vLLM_Preemption_Rate_High
expr: rate(vllm:num_preemptions[5m]) > 0.1 # >0.1 preemption/sec sustained
for: 5m
labels: { severity: warning }
- alert: vLLM_TTFT_p99
expr: |
histogram_quantile(0.99,
sum by (service, le) (rate(vllm:time_to_first_token_seconds_bucket[5m]))
) > 0.5
for: 5m
labels: { severity: critical }
- alert: vLLM_QueueBacklog
expr: vllm:num_requests_waiting > 50
for: 5m
labels: { severity: warning }
- alert: vLLM_PrefixCache_LowHitRate
expr: |
rate(vllm:prefix_cache_hits[1h])
/ rate(vllm:prefix_cache_queries[1h])
< 0.05
for: 1h
labels: { severity: info, tier: p3 }
annotations:
summary: "Prefix cache barely helping — investigate prompt structure"
- alert: vLLM_TP_Imbalance
expr: |
stddev by (pod) (DCGM_FI_PROF_SM_ACTIVE
* on(pod, namespace) group_left() kube_pod_labels{label_app="vllm"}
) > 0.05
for: 10m
labels: { severity: warning }
13. TGI / Triton-TRT-LLM¶
Two other LLM serving stacks worth covering:
13.1 TGI (Hugging Face Text Generation Inference)¶
Exposes a similar but smaller surface:
| Metric | Equivalent vLLM |
|---|---|
tgi_request_count |
(counter from Prom defaults) |
tgi_request_inference_duration_bucket |
vllm:request_inference_time_seconds_bucket |
tgi_batch_current_size |
vllm:num_requests_running |
tgi_queue_size |
vllm:num_requests_waiting |
13.2 Triton + TensorRT-LLM¶
Triton's metric surface (doc 04 §4.2) for batching, plus TensorRT-LLM-specific:
| Metric | Use |
|---|---|
nv_inference_batch_size_bucket |
Batch size distribution |
nv_inference_compute_inference_duration_us |
Compute time only |
nv_inference_queue_duration_us |
Time in queue |
For TensorRT-LLM, KV cache observability is exposed via TRT-LLM-specific gauges; the dashboard maps similarly to vLLM panels but the metric names differ.
14. Acceptance Checklist¶
- vLLM
:8000/metricsis scraped from every replica - L4 inference dashboard has TTFT, ITL, throughput, KV cache, queue panels
- Prefill vs decode separation visible in dashboards
- Spec decode acceptance rate plotted (if used)
- LoRA adapter usage tracked (if used)
- DCGM metrics joined to vLLM via
pod/namespacefor cross-correlation - LLM-specific alerts (KV saturation, preemption, TTFT) defined
- Per-tenant LLM SLOs (TTFT, ITL) defined for customer-facing services
15. Forward Pointers¶
- Doc 11: profiling LLM kernels — when metrics aren't enough
- Doc 13: per-tenant LLM observability for shared serving infra