How Does an LLM Serve Thousands of Requests? LLM 怎么同时服务几千个请求?
AI Infra Track Technology
This is the third note in my AI infrastructure learning series. The example throughout is a fictional startup building an AI coding agent with an open-source model such as Qwen. All token counts, timing examples, and dashboards are teaching assumptions, not measured benchmarks or a description of any one company's deployment. The diagrams show conceptual mechanisms rather than a complete production implementation.
00 One Model Is Not a Service
In the first two notes, I learned what happens inside one model computation and why the GPU can become a bottleneck. I knew about prefill, decode, KV cache, memory bandwidth, and batching. But then I ran into a different question.
What if our coding agent suddenly has a thousand users?
It's tempting to think the hard part is just making Qwen's forward pass faster. But a production system has to make decisions before the next forward pass begins: which request should run, where should its KV states live, what happens if memory fills up, and which model replica should receive the next user?
Making a model compute is one problem. Making thousands of users share that computation efficiently is another.
That second problem is LLM serving. My mental model shifted from a single model processing a single prompt to a system continuously allocating scarce compute and memory across many requests.
01 The Whole Request Lifecycle
Let's start with one user: “Find the bug in my Python repository and suggest a fix.” Here is the simplified path from the application to generated code.
Those two decisions are easy to mix up. A router might distribute requests across multiple independent model replicas. Once a request reaches one replica, that replica's scheduler decides when to execute it alongside other requests.
Inside an engine, the request can move between waiting and running states. It may also finish, be cancelled, or be preempted when resources are insufficient. That is why the arrows above are only the beginning of the story.
02 The Scheduler: Who Gets to Run Next?
Imagine A and B are already generating code. A new request, C, arrives with a long prompt. The engine doesn't have to wait until A and B finish their entire responses before considering C.
With continuous batching, the scheduler constructs a new execution workload across iterations. It can keep A and B decoding while admitting eligible work from C, subject to token budgets, memory limits, and the engine's scheduling policy.
The scheduler also needs a policy for choosing among waiting requests. First-Come, First-Served (FCFS) respects arrival order; a Longest Prefix Match (LPM) policy can favor requests likely to reuse more cached context. The latter may save computation, but it can also delay older requests.
I initially framed the choice as fairness versus efficiency. Then I noticed that the missing variable is actual latency: how many milliseconds do we save in prefill, and how long has the bypassed user already waited?
03 KV Cache: Not All Occupied Memory Is Equal
KV cache is both an accelerator and a constraint. It saves repeated attention-state computation, but it consumes finite GPU memory. The serving engine therefore needs to know not just how much memory is occupied, but who still needs it.
Reuse means a new request can use compatible cached prefix states instead of recomputing them. Eviction means discarding eligible inactive cached states to free space. Preemption means interrupting active execution when the engine cannot safely continue with its available resources.
The distinction matters. Reclaiming an inactive cached prefix may cause a future cache miss. Preempting a live request can delay a user who is already seeing output, and can require recomputing KV states when that request resumes.
To avoid mixing up requests, serving systems maintain logical request-to-token mappings and physical KV storage. A request's token sequence does not necessarily correspond to one contiguous region of GPU memory. Paged or block-based memory management makes allocations more flexible. PagedAttention is a well-known published example of this idea.
04 Admission, Waiting, and Preemption
There is another layer of decision-making before the GPU executes anything. If a server receives more work than it can handle, should it accept the work into a queue, let it run now, or reject it promptly?
Accept
The service allows the request into its system. That is not the same as having GPU resources ready now.
Wait
The request remains queued until there is sufficient execution budget and KV capacity.
Run
The scheduler admits eligible work into model execution, subject to configured limits.
Throttle / Reject
The frontend or gateway limits excess traffic instead of allowing an unbounded backlog.
A subtle problem appears during decode. A request that fits into memory now may need more KV capacity as it generates additional tokens. If admission is too aggressive, requests can repeatedly get retracted and rescheduled. The engine makes room, then may spend more GPU time recovering lost work.
Admitting more requests can make a system less productive if too many of them keep getting interrupted.
So admission control is partly about anticipating future resource demand, not merely checking whether memory is available at this instant.
05 GPU Workers and the Two Ways to Use Two GPUs
Once the scheduler selects work, a model worker or runner prepares the tensors, attention metadata, and KV mappings needed for GPU computation. The GPU performs the actual model operations; output tokens are sampled and returned for the next round of scheduling.
When we add a second GPU, we face an architectural choice.
Data parallelism avoids cross-GPU synchronization during ordinary independent inference, at the cost of duplicating model weights and having separate KV caches. Tensor parallelism can make an oversized model fit or speed up demanding computation, but its GPUs must coordinate, and communication can erase the benefit.
For our coding agent, I leaned toward data parallelism first: the Qwen model already fits on one GPU, and the problem is concurrent user demand. I would still measure throughput, tail latency, KV capacity, and cost before treating that choice as final.
06 Routing: The Least Busy GPU May Not Be the Fastest
Independent replicas introduce a new problem. Each GPU has its own working set of cached prefixes. If a coding agent repeatedly sends the same repository context, the replica that already processed it might save substantial prefill work.
But the cached replica might also be overloaded.
GPU 1 · Cache-rich
12 active requests
High loadGPU 2 · Cache-cold
3 active requests
Lower loadCompare three strategies:
| Strategy | Core idea | Blind spot |
|---|---|---|
| Round robin | Rotate requests between replicas | Requests have very different costs |
| Load-aware | Prefer workers estimated to be less busy | May throw away valuable cached prefixes |
| Cache-aware | Consider reusable prefixes and worker load | Can overload a cache-rich worker if tuned poorly |
One result surprised me: a higher cache hit rate can coexist with worse user latency.
| Hypothetical metric | Before | After |
|---|---|---|
| Prefix cache hit rate | 30% | 75% |
| p95 TTFT | 400 ms | 900 ms |
| Replica loads | Balanced | Uneven |
The cache-aware router may have concentrated too much traffic on a few replicas. Less prefill computation helped, but longer queues hurt even more. It would be a mistake to celebrate the hit-rate increase without checking the full latency distribution.
This is a simplified decomposition. Real engines can overlap work, and the time to first output also involves execution scheduling, sampling, and networking. Cache-hit percentages alone cannot predict how much TTFT will improve.
07 Diagnosing a Slow Service, Not Just a Slow Model
Once I had the full system in mind, one question became much easier: what do we investigate when users say the coding agent is slow?
| What users experience | First metrics to inspect | Possible mechanisms |
|---|---|---|
| No response for a long time | TTFT, queue wait, prefill duration | Routing imbalance, backlog, long prefill |
| Code starts flowing, then freezes | ITL distribution, preemption, forward duration | Scheduling delay, memory pressure, interference |
| GPUs are busy, but few requests finish | Throughput, goodput, work mix | Excess recomputation, ineffective batching, saturation |
One of our exercises had this imaginary dashboard:
p95 TTFT
p95 ITL
KV utilization
Preemptions
My first instinct was: “Decode is slow, so the GPU worker must be slow.” That's not precise enough.
The user sees inter-token latency (ITL), the interval between delivered output tokens. A long interval might mean the GPU forward computation itself takes longer. But it might also mean the request is waiting for its next turn, or that it was preempted and needs some KV state recomputed.
In the example above, I would first look for a relationship between preemption spikes and ITL spikes, then inspect per-iteration execution time. High KV utilization is a warning sign, not proof by itself. Likewise, high GPU utilization is not inherently bad, it matters whether the GPU is completing valuable work.
What Changes When We Limit Concurrency?
Suppose we reduce the maximum number of running requests per worker, from 32 to 24, and get these hypothetical results:
| Metric | Before | After |
|---|---|---|
| p95 ITL | 180 ms | 65 ms |
| p95 TTFT | 600 ms | 950 ms |
| Aggregate output throughput | 2,800 tok/s | 2,300 tok/s |
Existing users now see smoother code generation. New users wait longer. The service produces fewer output tokens each second. Is that a win?
It depends on the product. An interactive coding assistant might value smooth output enough to accept the trade-off. A background code-analysis pipeline might prefer total completed work. I would also measure goodput: how many requests meet the application's latency targets, rather than treating raw throughput as the whole answer.
08 The Mental Model I Wanted
At the beginning of this module, I pictured a request moving into a GPU, getting processed, and returning an answer. That was too linear.
Now I think of a serving engine as a set of decisions that repeat:
And every optimization has a cost somewhere else:
- Favor cached requests, and other users may wait longer.
- Admit more concurrent requests, and KV pressure may trigger repeated preemption.
- Reduce concurrent work, and streaming may become smoother while queueing gets worse.
- Split a model across GPUs, and communication may offset the benefit.
- Replicate models across GPUs, and routing determines how effectively their separate caches are used.
Workload → Bottleneck → Engineering trade-off → Customer value.
That's the chain I want to be able to explain, not just the vocabulary. If a coding-agent startup says users are waiting too long or generation keeps freezing, I now know to ask which kind of waiting, which resource is actually constrained, and what gets worse if we optimize it.
That takes me to the next question: once we understand the serving system, how does a real inference engine implement these mechanisms efficiently?
— References
- Yu et al., Orca: A Distributed Serving System for Transformer-Based Generative Models (OSDI 2022): iteration-level scheduling and batching.
- Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention (2023): block-based KV memory management.
- Zhong et al., DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving (2024): prefill/decode interference and service objectives.
- vLLM documentation: Automatic Prefix Caching: a public example of reusable KV prefixes and cache management.
Scope and limitations: These notes synthesize general serving concepts from public research and documentation. Implementations differ in scheduling, cache eviction, routing, batching, and preemption details. All diagrams and benchmark-like values are illustrative, not measurements or production claims. Prefix reuse requires compatible requests and cache residency; improved hit rate or GPU utilization does not guarantee improved latency.
这是我 AI 基础设施学习系列的第三篇。贯穿全文的例子是一家虚构的创业公司,用 Qwen 这样的开源模型做 AI 编程助手。所有 token 数量、时间例子和仪表盘都是为了讲清楚而设的假设,不是实测 benchmark,也不是在描述某一家公司的部署。图讲的是概念上的机制,不是一套完整的生产实现。
00 有一个模型,还不等于有一项服务
前两篇里,我学的是一次模型计算里面发生什么,以及 GPU 为什么会变成瓶颈。Prefill、decode、KV cache、显存带宽、batching,这些我都知道了。然后我碰到另一个问题。
如果我们的编程助手突然有了一千个用户呢?
很容易觉得,难的部分就是把 Qwen 的前向计算做快。但一套生产系统在下一次前向计算开始之前就要做决定:哪个请求该跑,它的 KV 状态放在哪,显存满了怎么办,下一个用户该进哪一个模型副本。
让模型算起来,是一个问题。让几千个用户高效地共用这次计算,是另一个问题。
第二个问题就是 LLM serving。我脑子里的图,从“一个模型处理一条提示词”,变成“一个系统在很多请求之间,持续分配紧缺的计算和显存”。
01 一个请求的整段生命周期
先看一个用户:“帮我在 Python 仓库里找到 bug,并给一个改法。”下面是从应用到生成代码的简化路径。
这两个决定很容易混在一起。路由器可以把请求分到多个彼此独立的模型副本上。请求进了某一个副本之后,这个副本的调度器再决定什么时候把它和其他请求一起执行。
在引擎内部,请求可以在等待和运行之间来回。它也可能结束、被取消,或者在资源不够时被抢占。所以上面的箭头只是故事的开头。
02 调度器:下一轮轮到谁?
想象 A 和 B 已经在生成代码。一个新请求 C 到了,提示词很长。引擎不必等 A 和 B 把整段回答生成完,才来考虑 C。
有了连续 batching,调度器会在一次次迭代里重新组这一轮要执行的工作。在 token 预算、显存上限和引擎的调度策略允许的范围内,它可以让 A 和 B 继续 decode,同时放进 C 里符合条件的那部分工作。
调度器还要有一套办法,在等待的请求里挑。 先到先服务(FCFS)尊重到达顺序;最长前缀匹配(LPM)可以优先那些可能复用更多缓存上下文的请求。后者可能省计算,但也可能让更早到的请求等更久。
我一开始把这个选择理解成公平对效率。后来发现缺的那个变量是真实的延迟:prefill 能省下多少毫秒,被插队的用户已经等了多久?
03 KV Cache:占着的显存并不都一样
KV cache 既是加速器,也是约束。它省下重复计算 attention 状态的工作,但吃掉有限的 GPU 显存。所以服务引擎要知道的不只是占了多少显存,还有谁还需要它。
复用是说,新请求可以用兼容的缓存前缀,而不必重算。驱逐是说,丢掉符合条件、已经不活跃的缓存,把空间腾出来。抢占是说,引擎靠现有资源没法安全地继续时,打断正在进行的执行。
这个差别要紧。收回一个不活跃的缓存前缀,以后可能会缓存未命中。抢占一个正在跑的请求,会让已经在看输出的用户变慢,请求恢复时还可能要重算 KV 状态。
为了不把请求混在一起,服务系统会维护“请求到 token”的逻辑映射,以及物理上的 KV 存储。一个请求的 token 序列,不一定对应 GPU 显存里连续的一块。按页或按块管理显存,分配会更灵活。PagedAttention 是这个想法一个广为人知的公开例子。
04 接收、等待和抢占
GPU 真正执行之前,还有一层决定。如果一台服务器收到的工作超过它处理得了的量,它该把工作接进队列、现在就跑,还是立刻拒绝?
接收
服务允许这个请求进入系统。这不等于 GPU 资源现在就准备好了。
等待
请求留在队列里,直到执行预算和 KV 容量都够。
运行
调度器把符合条件的工作放进模型执行,同时受配置好的上限约束。
限流 / 拒绝
前端或网关把多余的流量挡住,而不是让积压没有上限地涨。
Decode 期间会出现一个不容易注意到的问题。一个请求现在放得进显存,生成更多 token 之后可能需要更多 KV 容量。如果接收太激进,请求会反复被撤下来、再重新调度。引擎腾出了地方,然后可能花更多 GPU 时间去补回丢掉的工作。
接进更多请求,如果其中太多一直被打断,系统完成的活可能反而更少。
所以准入控制有一部分是在预估后面还要多少资源,不只是看这一瞬间显存够不够。
05 GPU Worker,以及两块 GPU 的两种用法
调度器选好工作之后,模型 worker(或 runner)会准备好 GPU 计算需要的张量、attention 元数据和 KV 映射。GPU 做实际的模型运算;输出 token 被采样出来,交回下一轮调度。
加上第二块 GPU 时,我们要做一个架构上的选择。
数据并行在普通的、彼此独立的推理里不必做跨 GPU 同步,代价是模型权重要复制一份,KV cache 也是分开的。张量并行可以让一个放不进单卡的模型放进去,或者加快很重的计算,但 GPU 必须互相配合,通信可能把好处吃掉。
对我们的编程助手,我更倾向先用数据并行:这个 Qwen 模型已经放得进一块 GPU,问题是同时来的用户。在把这个选择当成定论之前,我还是会去量吞吐、尾部延迟、KV 容量和成本。
06 路由:最闲的那块 GPU 不一定最快
独立副本带来一个新问题。每块 GPU 自己缓存着一组前缀。如果编程助手反复发来同一个仓库的上下文,已经处理过它的那个副本,可能省下相当多的 prefill。
但缓存命中的那个副本,也可能已经很忙。
GPU 1 · 缓存多
12 个正在处理的请求
负载高GPU 2 · 缓存冷
3 个正在处理的请求
负载较低比较三种策略:
| 策略 | 核心想法 | 看不见的地方 |
|---|---|---|
| 轮询 | 请求在副本之间轮着来 | 请求的成本可以差很多 |
| 看负载 | 优先估计上比较闲的 worker | 可能丢掉很有价值的缓存前缀 |
| 看缓存 | 同时考虑可复用的前缀和 worker 的负载 | 调得不好时,会把缓存多的 worker 压满 |
有一个结果出乎我意料:缓存命中率更高,可以和更差的用户延迟同时出现。
| 假设的指标 | 之前 | 之后 |
|---|---|---|
| 前缀缓存命中率 | 30% | 75% |
| p95 TTFT | 400 ms | 900 ms |
| 副本负载 | 比较均衡 | 不均衡 |
考虑缓存的路由器可能把太多流量集中到少数几个副本上。Prefill 的计算少了,但更长的队列伤得更多。只庆祝命中率上升、不去看完整的延迟分布,会看错。
这是一个简化的拆法。真实引擎里工作可以重叠,等到第一个输出的时间还包括执行调度、采样和网络。单看缓存命中的百分比,预测不了 TTFT 会改善多少。
07 诊断的是变慢的服务,不只是变慢的模型
脑子里有了整套系统之后,有一个问题变得好答很多:用户说编程助手慢,我们该去查什么?
| 用户感受到的 | 先看哪些指标 | 可能的机制 |
|---|---|---|
| 很久都没有响应 | TTFT、排队等待、prefill 花了多久 | 路由不均衡、积压、prefill 很长 |
| 代码开始往外流,然后卡住 | ITL 的分布、抢占、前向计算花了多久 | 调度延迟、显存压力、互相干扰 |
| GPU 很忙,但完成的请求很少 | 吞吐、有效吞吐、工作构成 | 重算太多、batching 没有效果、已经饱和 |
我们有一次练习用的是这样一张虚构的仪表盘:
p95 TTFT
p95 ITL
KV 利用率
抢占
我的第一反应是:“Decode 慢,所以 GPU worker 一定慢。”这不够精确。
用户看到的是 token 间隔(ITL),也就是交到手上的输出 token 之间隔了多久。间隔长,可能是 GPU 前向计算本身更久了。也可能是请求在等下一轮轮到自己,或者它被抢占了,有些 KV 状态要重算。
在上面的例子里,我会先看抢占的尖峰和 ITL 的尖峰有没有关系,再去看每一次迭代的执行时间。KV 利用率高是一个警告,本身不是证明。同样,GPU 利用率高并不自动是坏事,要看 GPU 完成的是不是有价值的工作。
把并发限下来,会变什么?
假设我们把每个 worker 同时运行的请求上限从 32 降到 24,得到这些假设的结果:
| 指标 | 之前 | 之后 |
|---|---|---|
| p95 ITL | 180 ms | 65 ms |
| p95 TTFT | 600 ms | 950 ms |
| 合计输出吞吐 | 2,800 tok/s | 2,300 tok/s |
已经在用的用户,代码生成更顺了。新用户等得更久。服务每秒产出的输出 token 变少了。这算赢吗?
取决于产品。交互式的编程助手可能愿意用这个取舍,换更顺的输出。后台的代码分析流水线可能更想要完成的总工作量。我还会去量有效吞吐(goodput):有多少请求达到了应用自己的延迟目标,而不是把原始吞吐当成全部答案。
08 我想要的那个心智模型
这一模块开始的时候,我把请求想成:进 GPU,被处理,返回答案。那条线太直了。
现在我把服务引擎看成一组反复发生的决定:
每一项优化,都会在别处有一笔成本:
- 优先缓存命中的请求,其他用户可能等得更久。
- 同时接进更多请求,KV 压力可能引发反复抢占。
- 把并发降下来,流式输出可能更顺,排队会更差。
- 把一个模型拆到多块 GPU 上,通信可能把好处抵消。
- 在多块 GPU 上复制模型,路由决定它们各自的缓存被用得有多好。
负载 → 瓶颈 → 工程上的取舍 → 客户价值。
我想能讲清楚的是这条链,不只是那一串名词。如果一家做编程助手的创业公司说,用户等得太久,或者生成总是卡住,我现在知道要问:等的是哪一种,真正紧的是哪一种资源,以及如果我们去优化它,什么会变差。
这就带到下一个问题:把服务系统看懂之后,一个真实的推理引擎是怎么把这些机制高效地做出来的?
— 参考
- Yu 等人,Orca:面向 Transformer 生成模型的分布式服务系统(OSDI 2022):迭代级的调度和 batching。
- Kwon 等人,用 PagedAttention 为大规模语言模型服务做高效显存管理(2023):按块管理 KV 显存。
- Zhong 等人,DistServe:为了优化有效吞吐,把大语言模型服务里的 prefill 和 decode 拆开(2024):prefill / decode 的互相干扰,以及服务目标。
- vLLM 文档:自动前缀缓存:可复用的 KV 前缀和缓存管理的一个公开例子。
范围和局限:这些笔记综合的是公开研究和文档里的一般服务概念。具体实现在调度、缓存驱逐、路由、batching 和抢占的细节上并不相同。所有的图和看起来像 benchmark 的数值都是示意,不是测量,也不是生产环境里的说法。前缀复用要求请求彼此兼容,而且缓存还在;命中率或 GPU 利用率变高,并不保证延迟变好。