How Does an LLM Serve Thousands of Requests? LLM 怎么同时服务几千个请求?

AI Infra Track Technology

AI Infra Learning Notes · 03 / 06
From request queues and GPU workers to KV memory, scheduling, and routing

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.

The router decides where a request goes. The scheduler decides what runs next inside the selected serving instance.

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.

A running batch changes over time. It is not a permanent group that must finish together.

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?

Scheduling policy → which work enters execution → queueing delay + GPU work → user latency and system throughput

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.

Evicting inactive cached states is different from interrupting a running request. The exact allocator may use blocks or pages, not literal individual-token slots.

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?

1

Accept

The service allows the request into its system. That is not the same as having GPU resources ready now.

2

Wait

The request remains queued until there is sufficient execution budget and KV capacity.

3

Run

The scheduler admits eligible work into model execution, subject to configured limits.

4

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.

If the model fits comfortably on one GPU and the main challenge is serving more users, independent replicas are often a useful baseline. But this is a hypothesis to benchmark, not a universal rule.

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.

A router should consider both cache locality and load. A predicted cache match may also be stale if the cached blocks have already been evicted.

Compare three strategies:

StrategyCore ideaBlind spot
Round robinRotate requests between replicasRequests have very different costs
Load-awarePrefer workers estimated to be less busyMay throw away valuable cached prefixes
Cache-awareConsider reusable prefixes and worker loadCan overload a cache-rich worker if tuned poorly

One result surprised me: a higher cache hit rate can coexist with worse user latency.

Hypothetical metricBeforeAfter
Prefix cache hit rate30%75%
p95 TTFT400 ms900 ms
Replica loadsBalancedUneven

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.

TTFT ≈ queueing + prefill + other costs

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 experienceFirst metrics to inspectPossible mechanisms
No response for a long timeTTFT, queue wait, prefill durationRouting imbalance, backlog, long prefill
Code starts flowing, then freezesITL distribution, preemption, forward durationScheduling delay, memory pressure, interference
GPUs are busy, but few requests finishThroughput, goodput, work mixExcess recomputation, ineffective batching, saturation

One of our exercises had this imaginary dashboard:

These numbers are invented. The point is to connect symptoms to mechanisms rather than guessing from a single metric.

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.

Observed ITL ≠ GPU forward duration alone

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:

MetricBeforeAfter
p95 ITL180 ms65 ms
p95 TTFT600 ms950 ms
Aggregate output throughput2,800 tok/s2,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:

New requests arrive → the router chooses a replica → requests wait or become eligible → the scheduler chooses token work → cache management allocates or reclaims memory → GPU workers execute → results stream back → the system schedules again

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

  1. Yu et al., Orca: A Distributed Serving System for Transformer-Based Generative Models (OSDI 2022): iteration-level scheduling and batching.
  2. Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention (2023): block-based KV memory management.
  3. Zhong et al., DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving (2024): prefill/decode interference and service objectives.
  4. 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 Infra 学习笔记 · 03 / 06
从请求队列和 GPU worker,到 KV 显存、调度和路由

这是我 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 里符合条件的那部分工作。

正在跑的 batch 会随时间变。它不是一个必须一起结束的固定小组。

调度器还要有一套办法,在等待的请求里挑。 先到先服务(FCFS)尊重到达顺序;最长前缀匹配(LPM)可以优先那些可能复用更多缓存上下文的请求。后者可能省计算,但也可能让更早到的请求等更久。

我一开始把这个选择理解成公平对效率。后来发现缺的那个变量是真实的延迟:prefill 能省下多少毫秒,被插队的用户已经等了多久?

调度策略 → 哪一份工作进入执行 → 排队延迟 + GPU 上的工作 → 用户延迟和系统吞吐

03 KV Cache:占着的显存并不都一样

KV cache 既是加速器,也是约束。它省下重复计算 attention 状态的工作,但吃掉有限的 GPU 显存。所以服务引擎要知道的不只是占了多少显存,还有谁还需要它。

清掉不活跃的缓存,和打断一个正在跑的请求,不是一回事。真正的分配器可能用的是块或页,不是字面上一个一个 token 的槽。

复用是说,新请求可以用兼容的缓存前缀,而不必重算。驱逐是说,丢掉符合条件、已经不活跃的缓存,把空间腾出来。抢占是说,引擎靠现有资源没法安全地继续时,打断正在进行的执行。

这个差别要紧。收回一个不活跃的缓存前缀,以后可能会缓存未命中。抢占一个正在跑的请求,会让已经在看输出的用户变慢,请求恢复时还可能要重算 KV 状态。

为了不把请求混在一起,服务系统会维护“请求到 token”的逻辑映射,以及物理上的 KV 存储。一个请求的 token 序列,不一定对应 GPU 显存里连续的一块。按页或按块管理显存,分配会更灵活。PagedAttention 是这个想法一个广为人知的公开例子。

04 接收、等待和抢占

GPU 真正执行之前,还有一层决定。如果一台服务器收到的工作超过它处理得了的量,它该把工作接进队列、现在就跑,还是立刻拒绝?

1

接收

服务允许这个请求进入系统。这不等于 GPU 资源现在就准备好了。

2

等待

请求留在队列里,直到执行预算和 KV 容量都够。

3

运行

调度器把符合条件的工作放进模型执行,同时受配置好的上限约束。

4

限流 / 拒绝

前端或网关把多余的流量挡住,而不是让积压没有上限地涨。

Decode 期间会出现一个不容易注意到的问题。一个请求现在放得进显存,生成更多 token 之后可能需要更多 KV 容量。如果接收太激进,请求会反复被撤下来、再重新调度。引擎腾出了地方,然后可能花更多 GPU 时间去补回丢掉的工作。

接进更多请求,如果其中太多一直被打断,系统完成的活可能反而更少。

所以准入控制有一部分是在预估后面还要多少资源,不只是看这一瞬间显存够不够。

05 GPU Worker,以及两块 GPU 的两种用法

调度器选好工作之后,模型 worker(或 runner)会准备好 GPU 计算需要的张量、attention 元数据和 KV 映射。GPU 做实际的模型运算;输出 token 被采样出来,交回下一轮调度。

加上第二块 GPU 时,我们要做一个架构上的选择。

如果模型在一块 GPU 上放得比较宽松,主要的挑战是服务更多用户,独立副本常常是一个有用的起点。但这是一个要拿去测的假设,不是普遍规则。

数据并行在普通的、彼此独立的推理里不必做跨 GPU 同步,代价是模型权重要复制一份,KV cache 也是分开的。张量并行可以让一个放不进单卡的模型放进去,或者加快很重的计算,但 GPU 必须互相配合,通信可能把好处吃掉。

对我们的编程助手,我更倾向先用数据并行:这个 Qwen 模型已经放得进一块 GPU,问题是同时来的用户。在把这个选择当成定论之前,我还是会去量吞吐、尾部延迟、KV 容量和成本。

06 路由:最闲的那块 GPU 不一定最快

独立副本带来一个新问题。每块 GPU 自己缓存着一组前缀。如果编程助手反复发来同一个仓库的上下文,已经处理过它的那个副本,可能省下相当多的 prefill。

但缓存命中的那个副本,也可能已经很忙。

路由器应该同时看缓存局部性和负载。如果缓存块已经被驱逐,预测中的缓存命中也可能是过期的。

比较三种策略:

策略核心想法看不见的地方
轮询请求在副本之间轮着来请求的成本可以差很多
看负载优先估计上比较闲的 worker可能丢掉很有价值的缓存前缀
看缓存同时考虑可复用的前缀和 worker 的负载调得不好时,会把缓存多的 worker 压满

有一个结果出乎我意料:缓存命中率更高,可以和更差的用户延迟同时出现。

假设的指标之前之后
前缀缓存命中率30%75%
p95 TTFT400 ms900 ms
副本负载比较均衡不均衡

考虑缓存的路由器可能把太多流量集中到少数几个副本上。Prefill 的计算少了,但更长的队列伤得更多。只庆祝命中率上升、不去看完整的延迟分布,会看错。

TTFT ≈ 排队 + prefill + 其他开销

这是一个简化的拆法。真实引擎里工作可以重叠,等到第一个输出的时间还包括执行调度、采样和网络。单看缓存命中的百分比,预测不了 TTFT 会改善多少。

07 诊断的是变慢的服务,不只是变慢的模型

脑子里有了整套系统之后,有一个问题变得好答很多:用户说编程助手慢,我们该去查什么?

用户感受到的先看哪些指标可能的机制
很久都没有响应TTFT、排队等待、prefill 花了多久路由不均衡、积压、prefill 很长
代码开始往外流,然后卡住ITL 的分布、抢占、前向计算花了多久调度延迟、显存压力、互相干扰
GPU 很忙,但完成的请求很少吞吐、有效吞吐、工作构成重算太多、batching 没有效果、已经饱和

我们有一次练习用的是这样一张虚构的仪表盘:

这些数字是编的。重点是把症状接到机制上,而不是凭一个指标去猜。

我的第一反应是:“Decode 慢,所以 GPU worker 一定慢。”这不够精确。

用户看到的是 token 间隔(ITL),也就是交到手上的输出 token 之间隔了多久。间隔长,可能是 GPU 前向计算本身更久了。也可能是请求在等下一轮轮到自己,或者它被抢占了,有些 KV 状态要重算。

观察到的 ITL ≠ 只有 GPU 前向计算的时间

在上面的例子里,我会先看抢占的尖峰和 ITL 的尖峰有没有关系,再去看每一次迭代的执行时间。KV 利用率高是一个警告,本身不是证明。同样,GPU 利用率高并不自动是坏事,要看 GPU 完成的是不是有价值的工作。

把并发限下来,会变什么?

假设我们把每个 worker 同时运行的请求上限从 32 降到 24,得到这些假设的结果:

指标之前之后
p95 ITL180 ms65 ms
p95 TTFT600 ms950 ms
合计输出吞吐2,800 tok/s2,300 tok/s

已经在用的用户,代码生成更顺了。新用户等得更久。服务每秒产出的输出 token 变少了。这算赢吗?

取决于产品。交互式的编程助手可能愿意用这个取舍,换更顺的输出。后台的代码分析流水线可能更想要完成的总工作量。我还会去量有效吞吐(goodput):有多少请求达到了应用自己的延迟目标,而不是把原始吞吐当成全部答案。

08 我想要的那个心智模型

这一模块开始的时候,我把请求想成:进 GPU,被处理,返回答案。那条线太直了。

现在我把服务引擎看成一组反复发生的决定:

新请求到达 → 路由器选一个副本 → 请求等待,或者变得可以执行 → 调度器选择 token 上的工作 → 缓存管理分配或回收显存 → GPU worker 执行 → 结果流回去 → 系统再调度一次

每一项优化,都会在别处有一笔成本:

  • 优先缓存命中的请求,其他用户可能等得更久。
  • 同时接进更多请求,KV 压力可能引发反复抢占。
  • 把并发降下来,流式输出可能更顺,排队会更差。
  • 把一个模型拆到多块 GPU 上,通信可能把好处抵消。
  • 在多块 GPU 上复制模型,路由决定它们各自的缓存被用得有多好。

负载 → 瓶颈 → 工程上的取舍 → 客户价值。

我想能讲清楚的是这条链,不只是那一串名词。如果一家做编程助手的创业公司说,用户等得太久,或者生成总是卡住,我现在知道要问:等的是哪一种,真正紧的是哪一种资源,以及如果我们去优化它,什么会变差。

这就带到下一个问题:把服务系统看懂之后,一个真实的推理引擎是怎么把这些机制高效地做出来的?

— 参考

  1. Yu 等人,Orca:面向 Transformer 生成模型的分布式服务系统(OSDI 2022):迭代级的调度和 batching。
  2. Kwon 等人,用 PagedAttention 为大规模语言模型服务做高效显存管理(2023):按块管理 KV 显存。
  3. Zhong 等人,DistServe:为了优化有效吞吐,把大语言模型服务里的 prefill 和 decode 拆开(2024):prefill / decode 的互相干扰,以及服务目标。
  4. vLLM 文档:自动前缀缓存:可复用的 KV 前缀和缓存管理的一个公开例子。

范围和局限:这些笔记综合的是公开研究和文档里的一般服务概念。具体实现在调度、缓存驱逐、路由、batching 和抢占的细节上并不相同。所有的图和看起来像 benchmark 的数值都是示意,不是测量,也不是生产环境里的说法。前缀复用要求请求彼此兼容,而且缓存还在;命中率或 GPU 利用率变高,并不保证延迟变好。

← Back