~learn.

ChatGPT LLM Serving, the whole design.

A serving stack that streams ~50 million output tokens a second to over a billion weekly users, by scheduling on tokens and GPU memory so each replica's every decode step serves a couple of hundred replies at once.

7 diagrams · 8 sections · revise in ~10 min · Download PDF prints it in the light theme, diagrams included

SQ-14 · ChatGPT LLM Serving · design sheethimanshukukreja.in/learn/sheets/bonus-14-chatgpt-llm-serving

At a glance#

Number Value What it drives
Users · messages 1.2B weekly · 4.3B / day (design) ~50,000 msg/s average, ~100,000/s at the 2× peak
Output tokens 25M / s avg · 50M / s peak 500 tokens a reply (assumption); decode GPUs
Input tokens 100M / s · half from prefix cache 2,000 tokens of context (assumption); prefill GPUs
KV cache (70B, bf16) 0.33 MB / token 2,000-token chat ≈ 655 MB; ~225 seqs per 4 × H100 replica
Per GPU ~1,500 out tok/s · ~3,500 prefill tok/s decode is memory-bound, prefill is compute-bound
Streams in flight ~1.8 million at peak 100,000/s × ~18 s a reply (Little's law)
Fleet (one 70B model, illustrative) ~75,000 H100s · ~115 MW ~$4.5M a day at $2.50/GPU-h (assumption)
Latency targets TTFT p95 < 1 s paid, < 2 s free · TPOT < 50 ms lanes by tier; people read ~5.6 tok/s

Architecture#

DiagramLLM serving on one page: everything keeps the GPUs busy and fair
%% title: LLM serving on one page: everything keeps the GPUs busy and fair
flowchart TD
  APP@{ icon: "sd:phone", label: "ChatGPT apps · web, mobile, desktop<br/>API clients" } ==> GW@{ icon: "sd:gateway", label: "Edge + API gateway<br/>TLS · SSE streams · auth<br/>token quotas per user per model" }
  GW ==>|"message"| CONV["Conversation service<br/>context in cache-friendly order<br/>count tokens"]:::service
  CONV --> HIST@{ icon: "sd:database", label: "Conversation store<br/>~10 TB a day · active cache" }
  CONV ==>|"context"| SAFE@{ icon: "sd:shield", label: "Safety classifiers<br/>input before · output per sentence" }
  SAFE ==>|"generate"| ROUTE["Router<br/>model · region · prefix affinity + overflow"]:::core
  ROUTE --> MINI["Mini-model pool<br/>fallback past limits"]:::service
  ROUTE ==> LANES@{ icon: "sd:queue", label: "Tier lanes<br/>paid · free · background" }
  LANES ==> REP["Model replicas · 4 × H100<br/>continuous batching · paged KV<br/>prefix cache"]:::hot
  REP ==>|"tokens"| TLOG@{ icon: "sd:cache", label: "Relay + token log<br/>numbered events · kept minutes<br/>streams back through the edge" }
  REP --> USE@{ icon: "sd:stream", label: "Usage + telemetry<br/>limits · billing · TTFT · cost" }
  USE --> FLEET@{ icon: "tech:kubernetes", label: "Fleet manager · Kubernetes<br/>forecast capacity · spot<br/>adds, drains replicas" }
  class APP client
  class GW edge
  class HIST store
  class SAFE,TLOG cache
  class LANES,USE queue
  class FLEET service
DiagramInside one replica: one step for the whole batch
%% title: Inside one replica: one step for the whole batch
flowchart TD
  W[["Waiting · from the tier lanes"]]:::queue --> ADM{"Prompt blocks fit,<br/>above the reserve?"}:::service
  ADM -->|"yes · reuse cached prefix blocks"| RUN["Running batch · ~225 seqs<br/>KV in 16-token blocks"]:::core
  ADM -->|"no"| W
  RUN --> STEP["One forward step<br/>prefill arrivals · 1 token for the rest<br/>reads all weights once"]:::hot
  STEP --> END{"Stop token, max tokens<br/>or cancelled?"}:::service
  END -->|"no · append K, V"| RUN
  END -->|"yes"| FREE(["Leave next step<br/>blocks freed, prefix kept"]):::ok
  RUN -.->|"out of blocks"| PRE(["Preempt the newest<br/>requeue, recompute later"]):::bad

Key flows#

DiagramOne message, keyboard to screen
%% title: One message, keyboard to screen
sequenceDiagram
  autonumber
  participant A as Meera's app
  participant E as Edge + gateway
  participant C as Conversation svc
  participant S as Router
  participant M as Replica r17
  A->>E: send (conversation c42, key m9)
  E->>E: auth · token quota ok (paid)
  E->>C: forward · open SSE stream
  C->>C: input check · 2,000-token context
  C->>S: generate (model, tier, prefix hash)
  S->>M: admit · prefix already cached here
  M->>M: prefill only ~300 new tokens
  M-)A: token 1 at ~0.42 s (via relay, edge)
  loop every ~35 ms until a stop token
    M-)A: next token · numbered event
  end
  M->>M: free KV blocks
  C->>C: output check · save reply
  C-)A: done
  Note over M: r17 writes ~224 other replies in the same steps
DiagramWi-Fi drops at token 212, then Stop at 260
%% title: Wi-Fi drops at token 212, then Stop at 260
sequenceDiagram
  autonumber
  participant A as App
  participant E as Edge server
  participant R as Relay + token log
  participant G as Replica
  A->>E: send message, open stream
  E->>R: subscribe to reply r88
  R->>G: generate r88
  loop each token
    G-)R: token n, appended to r88's log
    R-)A: event id n
  end
  Note over A,E: Wi-Fi drops at token 212
  A->>E: reopen · Last-Event-ID 212 (another edge)
  E->>R: subscribe to r88 after 212
  R-)A: 213 onwards from the log, then live
  Note over G: GPU never noticed
  alt user taps Stop at 260
    A->>E: stop
    E->>R: cancel r88
    R->>G: abort · free KV blocks next step
    R->>R: save 260 tokens, marked stopped
  else no reconnect within ~30 s
    R->>G: cancel · nobody is listening
  end
DiagramA replica dies with 225 replies in flight
%% title: A replica dies with 225 replies in flight
sequenceDiagram
  autonumber
  participant R as Relay + token log
  participant X as Replica r17
  participant S as Router
  participant N as Replica r42
  participant A as App
  X--xR: GPU fails · tokens stop
  Note over R: no tokens for ~2 s · health check confirms
  R->>S: resubmit each as continuation
  Note over S: context + tokens already sent · front of its lane
  S->>N: admit
  N->>N: prefill ~2,000 tokens (~0.15 s)
  N-)R: next token onwards
  R-)A: reply carries on after a short pause
  S->>S: stop routing to r17 · fleet replaces it

Data model#

DiagramCore entities
%% title: Core entities
classDiagram
  class Conversation {
    +conversationId : string · partition key
    +userId : string
    +tier : free / paid / business
    +model : string
  }
  class Message {
    +messageId : string
    +clientKey : string · idempotency
    +role : user / assistant
    +text : string · ~2.4 KB a turn
    +status : complete / stopped / flagged
  }
  class Reply {
    +replyId : string
    +replicaId : string
    +lastEventId : int
    +state : ReplyState
  }
  class TokenLogEntry {
    +replyId : string
    +n : int · SSE event id
    +token : string
  }
  class Sequence {
    +replyId : string
    +length : int · tokens so far
    +cachedTokens : int · prefix hits
    +arrived : timestamp · newest preempted first
  }
  class KVBlock {
    +blockId : int · 16 tokens
    +prefixHash : hash · full blocks only
    +refs : int
  }
  Conversation "1" --> "many" Message : turns
  Message "1" --> "1" Reply : answered by
  Reply "1" --> "many" TokenLogEntry : streamed as
  Reply "1" --> "1" Sequence : runs as
  Sequence "many" --> "many" KVBlock : shares prefix blocks

Lifecycle#

DiagramReply state machine
%% title: Reply state machine
stateDiagram-v2
  [*] --> QUEUED: admitted to a tier lane
  QUEUED --> SHED: free tier at capacity
  QUEUED --> RUNNING: blocks fit, prefill
  RUNNING --> PREEMPTED: out of blocks, newest
  PREEMPTED --> QUEUED: requeued, recompute
  RUNNING --> RESUMING: replica failed
  RESUMING --> QUEUED: continuation, front of lane
  RUNNING --> FINISHED: stop token or max tokens
  RUNNING --> STOPPED: user Stop or nobody for 30 s
  RUNNING --> FLAGGED: output check stops it
  FINISHED --> [*]
  STOPPED --> [*]
  FLAGGED --> [*]
  SHED --> [*]
  class FINISHED ok
  class SHED,FLAGGED bad
  class PREEMPTED,RESUMING queue

Decisions#

Decision Chose Over Because
Batching Continuous, per step (Orca) Static batches short replies leave, new ones join every step
KV memory 16-token blocks on demand (PagedAttention) Reserve the max per request old systems used only 20–38 % of KV memory
Routing Prefix affinity with overflow Round-robin or pure affinity skip prefill, but no hot replica
Queues Tier lanes, round-robin per user One queue free surge can't break paid TTFT
Quotas Tokens per user per model Requests requests vary 1,000× in size
Capacity Forecast the daily wave React to the queue a replica takes 3–10 min to serve
Streaming SSE with event ids + token log Resend the message a reconnect never repays the GPU

When it breaks#

Failure What users see Contain it with
GPU dies mid-reply a 1–2 s pause, then it carries on token log; resubmit as continuation
Phone switches network nothing Last-Event-ID; any edge resumes from the log
Launch at 5× demand fast mode, allowance, honest ETA pause background; separate pools; paid first
A region of four fails others at 100 %, cold prefixes run ≤ 75 % at peak; degradation ladder
Control plane overwhelmed (11 Dec 2024) all services degraded for hours serving keeps working on what it knows; phased rollouts, break-glass
Evening free-tier surge mini model or a clear limit lanes by tier; never a spinner

Say it in the interview#