Observability¶
What to scrape, what to alert on, and how to keep logs and Prometheus useful on a large fleet.
Scrape every model pod directly (a PodMonitor, or pod annotations), not through its Service: a Service scrape samples one endpoint at a time, and most questions here are per pod.
vLLM metrics that matter¶
| Metric | Type | Use |
|---|---|---|
vllm:num_requests_running |
gauge | Occupancy; autoscaling metric; capped at --max-num-seqs |
vllm:num_requests_waiting |
gauge | Queue on the pod; zero most of the time, so never the only scaling signal |
vllm:kv_cache_usage_perc |
gauge (0 to 1) | KV pressure; near 1 means preemptions. Older releases: vllm:gpu_cache_usage_perc |
vllm:num_preemptions_total |
counter | Requests pushed out of the KV cache |
vllm:iteration_tokens_total |
histogram | Its _count grows once per engine step: the progress signal for stall detection |
vllm:request_queue_time_seconds |
histogram | Time from arrival to first schedule; the user-facing queue |
vllm:time_to_first_token_seconds |
histogram | TTFT: queue plus prefill |
vllm:inter_token_latency_seconds |
histogram | Decode speed per stream (older releases: vllm:time_per_output_token_seconds) |
vllm:e2e_request_latency_seconds |
histogram | Request duration; input for choosing the drain timeout |
vllm:prompt_tokens_total, vllm:generation_tokens_total |
counters | Throughput |
vllm:prefix_cache_queries_total, vllm:prefix_cache_hits_total |
counters | Prefix cache hit rate |
vllm:request_success_total |
counter, by finished_reason |
Completions; length versus stop |
vllm:cache_config_info |
info | Confirms the running cache settings (for example enable_prefix_caching) |
Useful derived numbers:
capacity slots = count(ready endpoints) x max-num-seqs
occupancy = sum(num_requests_running) / capacity slots
prefix hit rate = rate(prefix_cache_hits_total) / rate(prefix_cache_queries_total)
queued minutes (7d) = sum_over_time((avg by (model_name)(vllm:num_requests_waiting) > bool 0)[7d:1m])
Queued minutes over a week is a better input for autoscaler floors than peak queue depth.
Fleet and card metrics¶
- Capacity versus allocatable per node from kube-state-metrics
(
kube_node_status_capacity{resource="habana_ai_gaudi"}andkube_node_status_allocatable): the difference is cards the device plugin withholds. - Card requests by pod and phase
(
kube_pod_container_resource_requests{resource="habana_ai_gaudi"}withkube_pod_status_phase): Pending pods with free cards in the fleet mean fragmentation. - Per-card utilization, memory and temperature need a Gaudi metrics exporter on each
node. Verify that it actually emits card series after installing it: an exporter that
cannot read the kubelet's pod-resources socket (
/var/lib/kubelet/pod-resources) can fail every collection without anyone noticing for months.
Alerts¶
monitoring.yaml
has these as Prometheus Operator rules:
| Alert | Condition | Catches |
|---|---|---|
| Engine stalled | Requests in flight, no engine step for 5 minutes | Hangs with /health 200 |
| Queueing | 90th percentile queue time above a threshold for 15 minutes | Under-provisioning; an autoscaler at its ceiling that reports "stable" |
| KV cache full | KV use above 95% for 10 minutes | Long-prompt pressure, preemptions |
| Cards withheld | Allocatable below capacity for 30 minutes | Unhealthy cards |
| Pods pending on cards | A Gaudi pod Pending for 30 minutes | Fragmentation, faulty nodes, quota |
| Restarting | More than two restarts in an hour | Crash loops, fatal device errors |
| Replica flapping | More than ten replica changes in two hours | Autoscaler instability |
Also worth alerting on, from other components: the gateway's count of upstream resets after the response started (cut streams, usually from missing drain), idle-timeout cuts, and any fatal-error gauge exported by custom HPU services.
Logs¶
Habana log volume. With ENABLE_CONSOLE=true and LOG_LEVEL_ALL=3, the runtime prints
every graph-compiler warning to stdout: tens of thousands of lines per minute during
compilation, which rotates the container log within minutes and takes the evidence of any
real failure with it. Either leave console logging off and keep the file logs (mounted from
the node, as in the template), or raise the level for the compiler and profiler loggers
only, which cut the volume by about 99% while keeping runtime warnings:
- {name: LOG_LEVEL_ALL, value: "3"}
- {name: LOG_LEVEL_SHARED_LAYER_API, value: "4"}
- {name: LOG_LEVEL_TPC_NODE, value: "4"}
- {name: LOG_LEVEL_SRAM_SLICE, value: "4"}
- {name: LOG_LEVEL_GC, value: "4"}
- {name: LOG_LEVEL_ROI_SPLITTER, value: "4"}
- {name: LOG_LEVEL_HW_TRACE, value: "4"}
- {name: LOG_LEVEL_PROF_SDK, value: "4"}
- {name: LOG_LEVEL_MEM_COHERENCE, value: "4"}
- {name: LOG_LEVEL_GC_TPC_FUSER, value: "4"}
LOG_LEVEL_ALL=4 silences all warnings; errors still print.
Probe and scrape noise. A router or autoscaler that scrapes /metrics on every pod
every few seconds, plus probes, can dominate a server's access log. In custom servers, drop
successful probe and scrape lines (match the exact path, not a prefix), and keep failures:
a readiness endpoint flipping to 503 is the line worth having.
Keep device logs past the container. Mount HABANA_LOGS to a per-pod directory on the
node. Prune those directories on the node, for example with a tmpfiles.d age rule.
Keep Prometheus healthy¶
- Per-pod series from pods that are gone. Components that re-export every model pod's
histograms with a
podlabel (some gateways do) and never forget deleted pods grow without bound under autoscaler churn. One such job reached millions of series and ran Prometheus out of memory. Drop the per-pod series you do not query withmetric_relabel_configs, and restart or fix the exporter. - Node exporter collectors you do not use (hwmon, softnet, per-CPU frequency, virtual network interfaces) add up quickly on large nodes; drop them at scrape time.
- Watch the watcher. A crash-loop check that reads kube-state-metrics through Prometheus cannot see Prometheus itself crash looping; check it from outside.