LLMModule 09 · Problems 16 and 18

Serving engines and observability

You can't run what you can't see, and you can't choose what you haven't measured fairly. This page designs the metrics an inference service needs, from time-to-first-token to cost per request, and then shows how to benchmark vLLM, TensorRT-LLM and SGLang so the comparison means something.

Problems covered16 (observability design), 18 (choosing a serving engine by benchmark)
Interview levelMid to senior. Both are system-design questions with no single right answer
Key ideasTTFT, TPOT, queue depth, KV-cache utilisation, prefix-cache hit rate, cost attribution, histograms and percentiles, goodput, open-loop load testing
Time to prep3–5 h reading, plus a day on one rented GPU for the benchmark exercise

Source: Vishal Singh's post of 25 inference-engineering problems · saved in sources/09-vishal-inference-25.md

Start here (no maths needed)

A dashboard tells you how a system is doing right now. A fair test tells you which of several systems is better for your job. Both are about measuring the right things in the right way.

Think of it like this

Think of a pizza delivery app. Most pizzas arrive in 30 minutes, but one order in a hundred takes two hours. The average looks fine, and that one customer is furious. So a good dashboard shows "how long did the slowest 1 in 100 wait", not just the average. It also shows how many orders are waiting, how full the ovens are, and what each pizza cost to make. Now suppose you want to pick the best of three pizza shops. A fair test orders the same pizzas, at the same busy time of day, to the same address. Ordering one plain pizza at 3 pm from one shop and ten fancy ones on Friday night from another tells you nothing.

How it works

  1. For every request, record how long until the first word came back, and how fast the rest arrived.
  2. Record the state of the machine: how many requests are waiting, how full its memory is, how often it reused earlier work.
  3. Show the slow tail (the worst 1%), not only the middle, because that's what angry users feel.
  4. To compare engines, run each one on the same model, same hardware and same realistic traffic, and push them until they miss your speed target.
  5. Pick the engine that serves the most traffic within the target, then check the things a benchmark can't show, like how easy it is to run.

Why robots care

A robot's onboard model must answer within a fixed time, every time. A robot team needs the same habits: log the slowest responses, not just the average, and test any new software on the same robot and same tasks before trusting it.

Try it

Words to know

TTFT
Time to first token: how long until the first piece of the answer arrives.
TPOT
Time per output token: the gap between later pieces of the answer.
Percentile
p99 is the value that 99% of requests beat. It describes the slow tail.
Throughput
How much work the system finishes per second, such as tokens per second.
Benchmark
A repeatable test that measures and compares systems under the same conditions.

Going deeper: everything below is the full version, with some maths. On a first read, skim the headings, the interview answers and the "Why it matters for robotics" box, and come back for the rest later.

Problem 16 · Design observability: TTFT, TPOT, queue depth, KV-cache utilisation, prefix-cache hit rate, cost per request

What the interviewer is testing: do you know what each metric means and how to compute it, which ones lead (warn before users notice) and which lag, how to store latencies so percentiles are correct, and how to turn GPU-hours into a cost per request?

The concepts

Per-request latency metrics. For a request with a prompt, a queue wait, and n output tokens:

TTFT  = time of first output token − time request arrived
        = queue wait + prefill (+ network and tokenisation)
TPOT  = (end-to-end latency − TTFT) / (n − 1)        mean gap after the first token
ITL   = each individual gap between consecutive tokens (TPOT is their mean)
E2E   = TTFT + (n − 1) × TPOT

TTFT is what a user feels as "is it working?". TPOT is reading speed. Measure both at two points: at the engine (what it controls) and at the client or gateway (what the user sees, including network and streaming). A gap between the two is a finding in itself.

Engine-state metrics. These lead: they move before latency does.

prefix hit rate  = prompt tokens found in cache / prompt tokens looked up
KV utilisation   = KV blocks in use / KV blocks allocated at startup

Store latencies as histograms, not averages. An average can't give you a percentile, and you can't average percentiles across replicas. A Prometheus histogram counts requests in fixed buckets; you sum buckets across replicas and estimate the quantile afterwards. The estimate is only as good as the buckets: Prometheus interpolates linearly inside a bucket, so a coarse bucket from 10 s to 30 s can put the p99 anywhere in that range. The demo shows this.

# p99 TTFT over 5 minutes, all replicas, per model (PromQL)
histogram_quantile(0.99,
  sum by (le, model_name) (rate(vllm:time_to_first_token_seconds_bucket[5m])))

# prefix-cache hit rate
sum(rate(vllm:prefix_cache_hits[5m])) / sum(rate(vllm:prefix_cache_queries[5m]))

vLLM's metrics page lists, among others, vllm:time_to_first_token_seconds, vllm:inter_token_latency_seconds, vllm:request_queue_time_seconds, vllm:num_requests_running, vllm:kv_cache_usage_perc and the prefix-cache hit and query counters. Names change between versions (counters usually get a _total suffix when exported), so read your server's /metrics output rather than copying names.

A worked answer: cost per request

Cost needs two inputs: what a GPU-hour costs, and how many GPU-seconds each request used. The price below is an assumption for illustration: say $2–4 per H100-hour, and use $3 for the arithmetic. Check your own contract or cloud price.

Step 1: time-share each decode step. If one step takes 30 ms and decodes 40 sequences, each sequence is charged 30 / 40 = 0.75 ms of GPU time for that step. Summing over a request's steps (and its share of prefill steps) gives its GPU-seconds. That's fair under batching, because a request that shares the GPU with 39 others shouldn't pay for the whole step.

Step 2: a steady-state check with Little's law.

Assumed: average 40 requests in flight on one GPU, each lasting 12 s
throughput        = 40 / 12 s ≈ 3.33 requests/s  = 12,000 requests/hour
GPU-s per request = 12 s / 40 = 0.3 GPU-s
cost per request  = $3 / 12,000 = $0.00025   (= 0.3 s × $3/3,600 s)
                  → $0.25 per 1,000 requests

Step 3: charge the idle time too. Traffic isn't flat. If over a day the fleet averages 40% of the load it was sized for, every served request carries the idle cost:

cost per request at 40% average load = $0.00025 / 0.4 = $0.000625

Report both numbers: the marginal cost (what one more request costs at the margin) and the fully loaded cost (total GPU bill ÷ requests served). The second is the one finance sees. Prefill and decode tokens cost different amounts per token, so for pricing you can fit cost per input token and per output token from the measured GPU-seconds.

The layers to instrument

LayerMetricsWhy
Client / gatewayTTFT and E2E as the user sees them, error rate, disconnects, rate-limit rejectionsThe truth about user experience
RouterRequests per replica, cache-aware routing decisions, retriesUneven load and cache misses start here
EngineQueue time, TTFT, TPOT, batch size, KV utilisation, preemptions, prefix hit rate, tokens in and outLeading indicators and capacity
GPUDCGM SM activity, memory used, power, temperature, ECC and Xid errorsHardware faults and real utilisation
BusinessCost per request and per million tokens, by tenant and modelPricing, capacity planning

Add per-request traces (OpenTelemetry has semantic conventions for generative-AI spans) that follow one request through gateway, router, queue, prefill and decode. Metrics tell you that p99 went up; a trace of a slow request tells you which stage did it.

Trade-offs

How to say it in the interview

I'd instrument four layers: client, router, engine and GPU, plus a cost view. The engine exports TTFT, TPOT and queue time as histograms, so I can sum across replicas and compute p50 and p99, with buckets dense around the SLO. The leading indicators are queue depth, KV-cache utilisation with preemption counts, and prefix-cache hit rate; they move before latency does, so they drive alerts and autoscaling. Cost per request comes from time-sharing each step's GPU time across the sequences in the batch, times the GPU-hour price, and I report both marginal and fully loaded cost including idle time. Per-request traces explain the outliers that the dashboards show.

Common mistakes

Problem 18 · vLLM vs TensorRT-LLM vs SGLang: benchmark on realistic workloads and pick one

What the interviewer is testing: can you design a benchmark that is fair, realistic and statistically sound, and do you know that the fastest engine on one chart is not automatically the right choice? A candidate who names a winner without asking about the workload is giving the wrong answer.

The concepts

The three engines, briefly. All three serve LLMs with continuous batching and paged KV caches, and all three can expose an OpenAI-compatible HTTP API, so one client can drive them all.

Each engine's performance changes from release to release, and each wins on some workloads in someone's benchmark. That's exactly why you measure on yours.

Goodput, not peak throughput. Peak tokens per second at unlimited batch size is easy to quote and useless for a latency-sensitive product. The useful number is the highest request rate at which the engine still meets your SLO. DistServe (Zhong et al., 2024) calls this goodput.

SLO (example, set yours):  p99 TTFT ≤ 1 s  and  p99 TPOT ≤ 50 ms
goodput = max request rate λ such that the SLO holds at λ
cost per 1M output tokens = ($ per GPU-hour × GPUs) / (output tokens/s at λ × 3,600) × 10⁶

Open loop vs closed loop. A closed-loop client sends the next request only when one finishes, so a slow server automatically receives less load and looks better than it is. Real users don't wait for each other. Use open-loop arrivals at a fixed average rate (Poisson is the usual choice), and sweep the rate.

The fairness checklist

Hold constantWhy it matters
Same checkpoint, tokenizer and chat templateA different template changes prompt length and output
Same precision and quantisation, or report separatelyFP8 in one engine and BF16 in another compares formats, not engines. Different FP8 recipes can differ in quality too
Same GPU model, count, driver, CUDA and parallelism (TP size)Hardware differences swamp engine differences
Same max context and sampling parametersContext limit sets KV memory; sampling changes cost
Same output lengthsFix max tokens and check actual output-token counts; one engine stopping early looks "faster"
Features on or off for allPrefix caching, chunked prefill, speculative decoding: enable in all or none, then test each separately
Equal tuning effort, from each engine's own docsA tuned engine against a default one is a tuning benchmark
One client tool for all three, run on a separate machineDifferent clients measure TTFT differently; a client on the GPU host steals CPU
Pinned versions, warm-up excludedReproducibility; compile and graph capture happen on first requests
Quality check on outputsRun a small eval on each engine's outputs. Fast and wrong isn't fast

Realistic workloads

Run at least three, matched to your product. Use length distributions from your own traffic logs if you have them; otherwise public conversation datasets give a starting shape.

WorkloadShapeWhat it stresses
ChatShort to medium prompts, a few hundred output tokens, wide spreadScheduling, TPOT under batching
Long-context / RAG8K–32K prompt tokens, short answersPrefill, chunked prefill, KV memory
Agentic / multi-turnLong shared system prompt, growing conversation historyPrefix caching, cache eviction
Structured outputJSON schema constrained decodingGrammar overhead per token
Offline batchThousands of requests at once, no SLORaw throughput only

A worked plan with real numbers

How many requests per point? A p99 from 1,000 requests rests on the slowest 10. One odd request moves it noticeably. Aim for several thousand requests per measurement point, and repeat each point 3 times to see the spread.

At λ = 5 req/s, 5,000 requests ≈ 1,000 s ≈ 17 min per point
(shorter runs at low λ, or report p95 when p99 is too noisy)

GPU budget. Suppose 5-minute runs (warm-up excluded) at 6 request rates:

3 engines × 3 workloads × 6 rates × 3 repeats × 5 min = 810 min ≈ 13.5 h
1 GPU   at an assumed $2–4/GPU-hour  → about $27–54
8 GPUs (TP=8 node)                   → about $216–432
plus engine setup time (TensorRT-LLM engine builds, graph capture) per model

Turning results into a decision, with made-up numbers. These numbers are invented to show the arithmetic; they say nothing about any real engine. Say on your chat workload, on one GPU at an assumed $3/hour, engine A meets the SLO up to 9 req/s and engine B up to 7.5 req/s, both averaging 300 output tokens per request.

A: 9 × 300 = 2,700 output tok/s   → $3 / (2,700 × 3,600) × 10⁶ ≈ $0.31 per 1M output tokens
B: 7.5 × 300 = 2,250 output tok/s → $3 / (2,250 × 3,600) × 10⁶ ≈ $0.37 per 1M output tokens
GPUs for a 90 req/s peak:  A → 10,  B → 12

A 20% capacity gap is real money at scale. But if B wins on your long-context workload, or A needs an hour-long engine build that breaks your autoscaling, the answer changes. Weigh it.

Beyond the benchmark

Trade-offs

ChoiceForAgainst
Benchmark on your trafficAnswers your questionNeeds logs and a replay tool; privacy work on prompts
Benchmark on public datasetsReproducible, shareableMay not match your length mix or prefix sharing
Pick one engineOne stack to learn and runLocked in if another engine pulls ahead
Abstract behind an OpenAI-compatible APISwap engines per modelLowest-common-denominator features, more to operate

How to say it in the interview

I wouldn't pick from published charts, because results change with each release and depend on the workload. I'd fix the model, precision, GPU and parallelism, drive all three engines with one open-loop client from a separate machine, and sweep the request rate on three workloads that match our traffic: chat, long-context and multi-turn with shared prefixes. The metric is goodput: the highest rate that still meets our p99 TTFT and TPOT targets, converted to GPUs and cost per million tokens. I'd tune each engine with equal effort from its own docs, repeat runs, and check output quality so no one wins by producing shorter or worse answers. Then I'd weigh the numbers against model coverage, startup time, features and how well the team can run it.

Common mistakes

Interactive: latency percentiles and histogram buckets

Histogram of time to first token for simulated requests (log scale from 10 ms to 100 s). Most requests wait briefly in the queue, then run prefill. A small "stall share" of requests is stuck behind something slow, such as a long prompt. Solid lines mark p50 and p99, the dashed line the mean. Seeded random numbers, so each setting gives the same result.

TTFT p50
–
TTFT p99
–
p99 ÷ p50
–
TTFT mean
–
Queue p99
–
TPOT p99
–
E2E p99
–
p99 from buckets
–

Try this: set Stall share to 0. TTFT p50 is about 171 ms and p99 about 659 ms (3.8×). Back at the default 2% with 5 s stalls, the p50 barely moves (174 ms) but the p99 jumps to 2.88 s (16.5×), and the mean of 301 ms describes nobody. With coarse buckets, a Prometheus-style estimate puts that p99 at 3.33 s; fine buckets give 2.94 s. Drop Requests to 500 and the p99 falls to about 0.8 s, because only 5 requests make up the top 1%: small samples make tails noisy. E2E p99 is long because a few answers run to 2,000 tokens, while TPOT p99 stays near 53 ms.

Practise it

Two exercises, one cheap and one on a rented GPU.

  1. Dashboard on a laptop-sized model. Serve a small model (about 0.5–1B parameters) with vLLM on a Colab GPU or a single rented GPU. Scrape its /metrics endpoint with Prometheus (or just poll it with a script). Drive it with a load tool at increasing request rates. Plot p50 and p99 TTFT from the histogram buckets, queue time, running batch size and KV-cache usage against time. Find the rate where queue time starts to climb: that's your capacity.
  2. Cost per request. From the same run, compute GPU-seconds per request by Little's law (in-flight ÷ throughput), then cost with an assumed GPU-hour price. Check it against total GPU time ÷ requests served.
  3. A small fair benchmark. On one rented GPU, serve the same small model in two engines (for example vLLM and SGLang) with the same precision and context length. Use one client, such as vLLM's vllm bench serve or NVIDIA's GenAI-Perf, pointed at each OpenAI-compatible endpoint. Sweep Poisson request rates on a chat-shaped and a long-prompt dataset, three repeats each. Plot p99 TTFT against rate and read off goodput for an SLO you choose.

What to measure: p50 and p99 TTFT and TPOT per rate, goodput at your SLO, output-token counts per engine (to confirm they match), run-to-run spread, and cost per million output tokens.

In this course

Why it matters for robotics

Self-check

A request took 6.2 s end to end, produced 201 tokens, and had a TTFT of 0.2 s. What is its TPOT?

(6.2 − 0.2) / (201 − 1) = 6.0 / 200 = 30 ms per token.

You have p99 TTFT from each of 8 replicas. Can you average them to get the fleet p99?

No. Percentiles don't average. Sum the histogram bucket counts across replicas and compute the quantile once.

Which three engine metrics lead latency, and why?

Queue depth (work is piling up), KV-cache utilisation with preemptions (the engine is about to evict or stall), and prefix-cache hit rate (a fall means more prefill work per request). They change before TTFT does.

A GPU at an assumed $3/hour serves an average of 40 requests in flight, each lasting 12 s. What is the cost per request?

Throughput is 40 / 12 ≈ 3.33 req/s, or 12,000 per hour. $3 / 12,000 = $0.00025 per request, before idle time.

Why is a closed-loop load test misleading for comparing engines?

The client waits for each response before sending the next, so a slower engine automatically receives less load. Queueing never builds up the way it does with real users. Use open-loop arrivals at fixed rates.

What is goodput?

The highest request rate (or throughput) at which the service still meets its latency SLO, for example p99 TTFT and TPOT targets. It's the fair basis for comparing engines and sizing fleets.

Resources