AI SWE Prep
0/10in AI/ML Engineering
AI/ML Engineering

Serving, Latency & Cost

Batching, KV-caches, streaming, and quantization. The engineering that decides whether your feature is affordable.

~15 minLesson 43 of 60
Your progressNot started

A model that looks good in a notebook can still be unusable in production. Users feel latency, finance feels token cost, and infrastructure feels bursty traffic. Serving is the engineering layer that turns inference into a dependable product endpoint.

For LLMs, performance is mostly about tokens: how many input tokens must be processed, how many output tokens must be generated, and how efficiently requests share the hardware.

The mental model

LLM latency has two major phases:

  1. Prefill — process the prompt tokens and build the KV cache.
  2. Decode — generate output tokens one at a time, reusing that cache.
def estimate_latency(input_tokens, output_tokens, prefill_tps, decode_tps, overhead_ms=80):
    prefill_ms = (input_tokens / prefill_tps) * 1000
    decode_ms = (output_tokens / decode_tps) * 1000
    return overhead_ms + prefill_ms + decode_ms

print(estimate_latency(2_000, 300, prefill_tps=40_000, decode_tps=120))

The numbers vary by model, hardware, batch size, and provider, but the shape is stable: long prompts hurt prefill; long answers hurt decode.

Prefill, decode, and the KV cache

Anatomy of one LLM request
  1. 1
    Prefillencode prompt → fill KV cache
  2. 2
    Decode loopone token at a time, reuse KV
  3. 3
    Continuous batchingpack many requests per step
  4. 4
    Streamemit tokens as they're produced
Decode is memory-bound; batching + KV reuse are where throughput is won.

During prefill, the model reads the whole prompt and stores attention keys and values in a KV cache. During decode, each new token can attend to previous tokens through that cache instead of recomputing the entire prompt.

The cache is why conversation history is expensive twice: it consumes context window and memory on the serving side. More concurrent long-context requests mean less room for batching and more pressure on GPU memory.

Continuous batching

GPUs want batches. Users want low latency. Continuous batching reconciles the two by adding and removing requests from a live batch as tokens are decoded.

Without it, a short request can sit behind a long one. With it, the server keeps the GPU busy while still streaming tokens back to each user.

The trade-off is scheduling complexity: batching improves throughput, but too much waiting to form a batch increases time-to-first-token.

Streaming responses

Streaming does not necessarily reduce total generation time. It reduces perceived latency by sending the first tokens as soon as decode begins.

Use streaming for chat, writing, and support answers where partial text is useful. Avoid it for tiny JSON responses, background jobs, or flows where the caller needs the complete object before doing anything.

Quantization

Quantization stores weights in fewer bits, such as 8-bit or 4-bit instead of 16-bit. It reduces memory use and can improve throughput, especially when memory bandwidth is the bottleneck.

The cost is possible quality loss and hardware-specific behavior. Quantize only after evaluating the real task, not just a generic benchmark.

Caching

The cheapest inference is the one you skip.

  • Exact cache — same normalized prompt, same answer.
  • Semantic cache — embed the request and reuse answers for near-duplicates.
  • Prefix cache — reuse KV state for shared prompt prefixes when the serving stack supports it.
  • Retrieval cache — cache search results or document chunks separately from the generated answer.

Cache only when the answer is safe to reuse for that user, time, and permission set.

Pattern recognition

Variations

Worked problems

DesignEasy
  • Latency
  • Tokens

Reduce p95 latency

A RAG endpoint has p95 latency of 7 seconds. Traces show 2 seconds in retrieval, 1 second in prefill, and 4 seconds in decode. Users need the endpoint to feel responsive under 3 seconds. What do you change first?

Approach. Attack the largest component, then separate perceived latency from total latency. Decode dominates, so reduce output tokens or stream.

Show solution

Start with:

  1. Stream output to reduce time-to-first-token even if total latency remains.
  2. Lower max_tokens and ask for a concise answer; 4 seconds of decode means the model is producing too much text.
  3. Use a faster/smaller model for straightforward questions and escalate only uncertain cases.
  4. Cache retrieval for repeated questions or common documents.
  5. Profile retrieval next, because 2 seconds is also too high for interactive RAG.
def latency_budget(trace):
    total = sum(trace.values())
    return {name: seconds / total for name, seconds in trace.items()}

print(latency_budget({"retrieval": 2.0, "prefill": 1.0, "decode": 4.0}))

Decode is about 57% of latency. Streaming is the fastest product improvement; shorter answers and model routing reduce actual cost and latency.

Complexity. O(1). The real discipline is using traces instead of guessing.

DrillMedium
  • Throughput
  • Cost

Estimate throughput and cost

A GPU serving stack decodes 2,400 tokens per second across all active requests. The average response is 300 output tokens. GPU rental costs $3.60 per hour. At 70% safe utilization, estimate completed responses per second and GPU cost per 1,000 responses, ignoring prefill.

Approach. Convert token throughput into response throughput, apply safe utilization, then spread hourly cost across hourly responses.

Show solution
def serving_cost(decode_tps, output_tokens, hourly_cost, utilization):
    responses_per_second = (decode_tps * utilization) / output_tokens
    responses_per_hour = responses_per_second * 3600
    cost_per_1000 = hourly_cost / responses_per_hour * 1000
    return responses_per_second, cost_per_1000

rps, cost = serving_cost(2_400, 300, 3.60, 0.70)
print(round(rps, 2), round(cost, 4))

The endpoint completes about 2_400 * 0.70 / 300 = 5.6 responses per second. That is 20,160 responses per hour, so GPU cost is about $0.18 per 1,000 responses before prefill, idle time, retries, networking, and overhead.

Complexity. O(1). This is napkin math, but it catches unrealistic cost claims early.

DesignMedium
  • Streaming
  • Batching

Stream or batch?

Decide whether to stream responses for three endpoints: a chat assistant, a nightly summarization job over 50,000 tickets, and an API that extracts a 12-field JSON object from an invoice.

Approach. Stream when partial output helps the user. Batch when throughput or complete structured output matters more than perceived latency.

Show solution
def serving_mode(endpoint: str) -> str:
    if endpoint == "chat_assistant":
        return "stream"
    if endpoint == "nightly_ticket_summary":
        return "batch"
    if endpoint == "invoice_json_extraction":
        return "non_streaming_request_response"
    raise ValueError(endpoint)

Chat should stream because users can read as the answer forms. The nightly job should batch aggressively for throughput and cost. Invoice extraction should wait for the full validated JSON object; streaming partial JSON complicates callers and adds little value.

Complexity. O(1). The design criterion is whether partial tokens are useful to the consumer.

The classic pitfall

Your progressNot started