Part III - Production inference
Observability for LLM Serving
Lesson 10 split prefill and decode onto separate pools so each could scale on its own bottleneck. But the moment you disaggregate, the question "is it healthy?" stops having a single answer: prefill can be starved while decode is idle, or the GPUs can be pinned at 100% utilization while users wait twenty seconds for a first token. A green HTTP 200 tells you almost nothing about any of that. This lesson builds an observability stack that sees an LLM workload in its own language — token timing, queue depth, GPU memory pressure, and output quality — so that when something degrades, the telemetry points at the exact path that broke.
New capability: An observability stack — logs, metrics, traces, and quality signals — that decomposes a request along four paths and tells you which one is out of bounds, instead of one aggregate "up/down".
1 · "Healthy" is four paths, not one status code
A classic web service is healthy when requests return 2xx quickly and the error rate is low. That model breaks for LLM serving because a single request is not one operation — it is a multi-stage pipeline that produces output incrementally over seconds. The HTTP status fires once, at the start, when the server accepts the streaming connection. Everything that actually matters to the user — how long until the first token, how smoothly the rest stream, whether the answer is correct — happens after the 200 is already sent.
So the mental model for the rest of this lesson: an LLM request is healthy only when four independent paths are all inside their bounds. Each can fail while the others look fine, and each has its own telemetry.
The discipline this lesson teaches is to instrument all four and to correlate them by the same dimensions — model, version, tenant, route, prompt length, output length — so that "TTFT spiked" can be joined to "HBM hit 95%" on the same trace.
2 · The token path — and why a single latency is a lie
Two metrics carry the token path. TTFT (time to first token) is the elapsed time from when the request is accepted to when the first output token is emitted; it is dominated by queue wait plus prefill (the one-shot forward pass over the whole prompt, lesson 06). TPOT (time per output token) is the average gap between successive tokens once generation has started; it is dominated by decode (one token at a time, memory-bandwidth bound). End-to-end latency for a streaming response is therefore:
total = TTFT + TPOT × (output_tokens − 1)
This decomposition is the whole point, because the two terms move for completely different reasons. TTFT grows when the queue is deep or the prompt is long (prefill cost scales with prompt length). TPOT grows when decode is contended — more concurrent sequences sharing the GPU, or KV-cache pressure forcing evictions. A single aggregate latency number cannot tell these apart.
Throughput (tokens/sec, aggregated across all concurrent sequences) is the third token-path metric and the one that pays the bills: it is the denominator of your $/1M-tokens cost. Critically, throughput and per-request TPOT trade against each other — larger batches raise total tokens/sec but can raise each individual user's TPOT. You cannot reason about that trade if you only watch one of them, which is exactly the knob the widget in §4 makes draggable.
3 · The GPU path — why utilization alone lies
The instinct from VM/container ops is to watch one number: GPU utilization. DCGM (Data Center GPU Manager) is NVIDIA's host-level metrics exporter — it runs as a DaemonSet on every GPU node and scrapes per-GPU counters into Prometheus. The trap is that DCGM's headline "GPU utilization" (DCGM_FI_DEV_GPU_UTIL) only means "a kernel was running on the device during the sample window." It can read 100% while the GPU does almost no useful token work — for example, when the batch is tiny and decode is memory-bandwidth bound, or when tensor-parallel ranks (lesson 07) spend the window blocked in a collective waiting on a slow peer.
So the GPU path needs more than one counter:
4 · The trace — spans, segmentation, and the streaming gap
Metrics tell you a path is out of bounds in aggregate; a trace tells you where a single slow request spent its time. A trace is a tree of spans — timed intervals — and the LLM-specific work is to emit a span for each stage of the inference pipeline, each tagged with model and model_version attributes so you can slice by deployment:
A decomposed trace for the 15.2-second request from §2 reads like this — and immediately localizes the cost:
request (15.2 s) ├─ gateway 10 ms auth + route, model=llama-70b v=2026-05 ├─ queue 150 ms waited for a decode slot ┐ ├─ prefill 50 ms 120-token prompt ┘ queue + prefill ≈ TTFT 200 ms ├─ decode 14.97 s 500 output tokens @ ~30 ms each (499 gaps) ← the time lives here └─ stream (overlaps decode) tokens flushed to client as they are produced
The lesson the trace teaches: this request is not a prefill or queue problem — 14.97 of 15.2 seconds is legitimate decode for 500 tokens. No amount of adding prefill replicas would help. Contrast that with a request whose queue span is 3 s: same total latency, completely different fix (the runtime is saturated — autoscale decode, lesson 09). Without the spans you cannot tell these apart from the outside.
gen_ai.* namespace — e.g. gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.response.finish_reasons — so your spans are portable across tools and your dashboards keep working when you swap a backend. Adopting the convention is what lets a generic trace UI know that this span is an LLM call and render token counts, not just a black-box duration. Caveat for 2026: this is an actively evolving spec — most of the GenAI conventions are still marked Development (experimental), so attribute names and the opt-in capture of prompt/response content can still shift between releases. Pin the convention version you target and expect to migrate, rather than treating today's names as frozen.The single most common tracing bug here is dropping the streaming span. If the trace ends when the request is accepted (or when the first token leaves), the seconds of decode that the user actually experiences are invisible — TTFT looks fine and the trace looks short, yet users complain. You must keep the span open across the whole streamed response and record first-token time within it.
5 · The quality path — green metrics, wrong answers
The first three paths are about the request being served; the fourth is about it being correct. This path has no equivalent in classic ops, and it is the one that ships silent regressions, because operational dashboards stay green while the model gets worse. A prompt-template edit, a retrieval index that went stale, a quantized weight swap, or a base-model version bump can all leave TTFT, TPOT, and HBM untouched while answer quality falls off a cliff.
So the quality path needs its own continuously-tracked signals, joined to the same model + version attributes as the spans: eval scores on a held-out task set run against live traffic samples; refusal rate (fraction of requests the model declines — a spike often means an over-tightened safety prompt, a drop means guardrails loosened); and hallucination rate (sampled, judged by a rubric or a stronger judge model). These are sampled, not measured on every request, and they are the bridge into the next lesson — guardrails are the enforcement layer that acts on what the quality path observes.
6 · Failure modes and the checklist
Failure modes
- Only node-level GPU utilization is visible. You see a green DCGM utilization dashboard and conclude the system is healthy, but queue wait and token latency are uninstrumented — so saturation and a degraded TTFT stay mysterious. Signal: users report slowness while every GPU graph looks maxed-out-and-fine. Add queue depth, TTFT, and TPOT.
- Tracing drops the streaming span. The trace ends at request acceptance or first token, so the seconds of decode the user lives through never appear, and TTFT/TPOT can't be reconstructed from the trace. Signal: short, healthy-looking traces alongside real complaints about response speed. Keep the span open across the whole stream.
- Quality regressions ship because ops stays green. A template/index/weight change tanks answer quality with zero movement in latency or GPU metrics, so it passes every operational gate. Signal: rising thumbs-down or support tickets with no latency anomaly. Track eval/refusal/hallucination rates per version.
- Averaging across segments. A single TTFT distribution blends 100-token and 8k-token prompts into a meaningless middle that hides the prefill problem. Signal: a stable aggregate metric while specific cohorts suffer. Segment TTFT by prompt length and TPOT by output length.
- Metrics that can't be joined. GPU, token, and request metrics carry inconsistent labels, so "TTFT spiked" can never be correlated with "HBM hit 95%." Signal: every dashboard looks plausible in isolation and no one can build the causal chain. Tag everything with the same model/version/tenant/route dimensions.
Implementation checklist
- Do you emit request, queue, prefill, decode, and streaming spans, each tagged with
modelandmodel_version(ideally via the OpenTelemetrygen_ai.*conventions)? - Are TTFT and TPOT tracked as separate distributions, segmented by prompt length and output length respectively?
- Is the streaming span kept open for the full response, with first-token time recorded inside it?
- Do you correlate GPU utilization, SM occupancy, HBM usage, and cache pressure with the token-path and queue metrics on shared labels?
- Is there a derived tokens/sec-per-utilization signal to catch the "100% busy, low output" pathology?
- Are eval, refusal, and hallucination rates sampled and tracked per model version, joined to the operational metrics?
- Is prompt/completion logging governed by sampling, redaction, access control, retention, and tenant policy?
Checkpoint exercise
Where this points next
This lesson built the seeing layer — telemetry that decomposes a request into request, token, GPU, and quality paths and tells you which one is out of bounds. The quality path in particular only observes: it reports refusal and hallucination rates, but it does not stop a bad output from reaching the user. Lesson 12, Safety, Quality, and Guardrails, adds the acting layer — input and output filters, policy enforcement, and quality gates that consume the very signals you just instrumented and block, rewrite, or escalate before the response ships. For the broader monitoring discipline behind all of this, see ML system monitoring.
gen_ai.* conventions so GPU, token, and quality signals can be correlated. Never drop the streaming span, never read GPU utilization alone, and govern prompt/completion logging by privacy policy — observability that leaks data is just a new incident.Interview prompts
- Why is an HTTP 200 insufficient to call an LLM request healthy? (§1 — the status fires once at stream acceptance; TTFT, TPOT, GPU pressure, and answer quality all happen afterward, so the four paths must be instrumented independently.)
- Define TTFT and TPOT and write end-to-end latency in terms of them. (§2 — TTFT = accept-to-first-token (queue + prefill); TPOT = average inter-token gap (decode); total = TTFT + TPOT × (output_tokens − 1).)
- Why is a single p50 latency useless for diagnosis? (§2 — it blends prefill-bound and decode-bound requests; a long-prompt request and a long-output request can share a total but need opposite fixes. Split and segment TTFT by prompt length, TPOT by output length.)
- Why is GPU utilization a misleading health metric, and what do you watch instead? (§3 — it only means "a kernel ran"; it reads 100% during memory-bound decode or collective stalls. Watch SM occupancy, HBM usage, power/thermals, and tokens/sec ÷ utilization.)
- What spans should an LLM trace emit, and which one is most often missed? (§4 — gateway, queue, prefill, decode, stream, tagged with model + version; the streaming span is the one most commonly dropped, hiding the decode time users feel.)
- What does the OpenTelemetry GenAI convention buy you? (§4 — a standard
gen_ai.*attribute namespace for model, token counts, and finish reasons, making traces portable across tools and dashboards stable across backend swaps.) - How can a release pass every operational gate yet still be a regression? (§5 — quality lives on a separate path; a template/index/weight change can leave latency and GPU metrics untouched while answers degrade. Track eval/refusal/hallucination per version, governed by privacy policy.)