【AI专栏】图解Transformer - 第06章:LLM推理服务

📅 2026/7/11 3:05:17 👁️ 阅读次数 📝 编程学习
【AI专栏】图解Transformer - 第06章:LLM推理服务

博主介绍:程序喵大人

  • 35 - 资深C/C++/Rust/Android/iOS客户端开发
  • 10年大厂工作经验
  • 嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手
  • 《C++20高级编程》《C++23高级编程》等多本书籍著译者
  • 更多原创精品文章,首发gzh,见文末
  • 👇👇记得订阅专栏,以防走丢👇👇
    😉C++基础系列专栏
    😃C语言基础系列专栏
    🤣C++大佬养成攻略专栏
    🤓C++训练营
    👉🏻个人网站

1. 图解 LLM 推理服务

第五章看的是一个请求内部的成本:prefill、decode、KV Cache、显存、带宽。那是从一个请求自己的视角出发的。

这一章把视角拉到服务端:同时来了一百个用户,每个人发的 prompt 长短不一、生成长度也不同——模型服务怎么接住这些请求?

答案是一套系统工程:请求先进队列,Admission Control 控制入口,Scheduler 决定谁先跑,batch 把多个请求合在一起送进 GPU,continuous batching 在生成过程中动态调整 batch 组成,KV Cache 资源池管理每个请求的缓存分配,Router 把请求路由到合适的 worker,streaming 让用户边等边看。

这些组件串起来,就是一个完整的 LLM 推理服务系统。每个环节要解决的核心问题是同一个:怎么在有限的 GPU 资源下,让尽可能多的用户获得尽可能快的体验。

2. 多人同时提问

真实的 LLM 服务不是一个用户用完另一个用户再来。高峰期可能有成百上千个请求同时到达。

每个请求的特征都不一样:有的 prompt 只有几个 token(“写首诗”),有的 prompt 有几千个 token(“总结这篇论文”)。生成长度也不同——有的需要 20 个 token 的短回复,有的需要 2000 个 token 的长文章。

GPU 资源是有限的。一块 GPU 的显存装得下模型权重之后,剩余空间决定了同时能容纳多少请求的 KV Cache。算力也有上限——同一时刻能并行处理的 token 数量是有限的。

所以服务端面对的根本问题是:很多请求抢有限的 GPU。谁先跑、怎么跑、怎么不让某些请求饿死——这就是推理服务系统要做的调度。

3. 请求先进入队列

请求到达服务端以后,通常不会立刻进 GPU。它先进入一个队列等着。

为什么要排队?因为 GPU 当前可能正在忙,或者 KV Cache 空间不够再塞一个新请求。队列是系统的缓冲层——让请求有地方等,而不是直接被丢弃。

队列里的每个请求带着自己的元信息:到达时间、prompt 长度、预估生成长度、优先级。这些信息后面会被 Scheduler 用来做调度决策。

排队时间(queue time)是用户体验的一部分。用户发出请求到第一个字出现,中间等的时间 = queue time + prefill time。系统越忙,queue time 越长。如果排队太久,用户就会觉得"模型很慢"——其实模型跑起来以后并不慢,慢在排不上队。

4. Admission Control

队列也不是无限长的。如果请求一直进来但 GPU 一直忙不过来,队列堆积会越来越深,所有人的等待时间都会变长,系统最终可能崩溃。

Admission Control 就是入口闸门。它在请求进入队列之前检查系统状态:GPU 利用率、KV Cache 剩余空间、当前队列长度、错误率。如果系统已经接近容量上限,Admission Control 会采取措施——限流、拒绝、降级、或者告诉客户端稍后重试。

这看起来不太友好,但保护系统容量是为了保证已经在跑的请求不被拖慢。如果不做限制,新涌入的请求会和正在生成的请求抢 KV Cache 空间和 GPU 时间,所有人的体验一起变差。

Admission Control 是 LLM 服务和传统 Web 服务的一个关键区别:LLM 推理占用的资源量级远大于普通 API 请求,一个长上下文的请求可能占几 GB 显存。所以容量管理必须做得更细。

5. Scheduler:决定谁先跑

队列里有多个请求等着,GPU 每一轮能处理的 token 数量有限——Scheduler 的工作是决定这一轮让谁上。

调度不是简单的先来先服务。Scheduler 要考虑的因素很多:

请求状态不同。有的请求是新来的,需要做 prefill(计算量大,但只跑一次);有的请求已经在 decode 中,每步只需要处理一个 token。把一个需要 prefill 的长 prompt 请求和一堆正在 decode 的请求放在同一轮,资源分配就要重新规划。

优先级可能不同。付费用户、内部服务、免费用户的优先级设计不同。

资源约束实时变化。KV Cache 剩余多少、GPU 显存还够不够再加一个请求——这些都是 Scheduler 每一轮要检查的。

具体用什么调度策略,不同系统的选择不同。核心逻辑是一样的:在有限的资源下,每轮选出一组能合理共存的请求送进 GPU。

6. Batch:把请求合在一起跑

第五章讲的是一个请求独占 GPU 的情况。但实际服务中,一次只跑一个请求太浪费了——GPU 的大量算力闲置。

Batch 的思路是把多个请求合在一起送进 GPU。多个请求的矩阵运算可以拼在一起,用一次 kernel launch 同时处理。GPU 的计算单元被更充分地填满,单位时间能处理更多 token。

要注意的是:batch 里的请求只是共享 GPU 计算资源,它们的数据是隔离的。请求 A 看不到请求 B 的上下文,KV Cache 也是各自独立的。batch 不是把多个请求的文本拼成一个长序列。

Batch 提高了吞吐量(单位时间完成更多请求),但也带来了调度复杂性:batch 里的请求长度不一、生成进度不同,如何组合最高效是一个持续的工程问题。

7. Continuous Batching

传统的 static batching 是把一组请求凑齐了一起跑,跑完了再换下一组。问题是:batch 里最短的请求可能 10 步就生成完了,但最长的要跑 500 步。先完成的请求只能干等着,GPU 的对应计算槽位在空转。

Continuous batching 解决这个问题:当 batch 里某个请求生成结束,它的槽位立刻释放,Scheduler 从队列里拉一个新请求填进来。batch 不再是固定一组人从头跑到尾,而是边跑边换。

每一轮 decode iteration,Scheduler 都有机会调整 batch 的组成:完成的请求离开,新请求加入,需要 prefill 的请求被插入。batch 变成了一个动态的容器。

收益很直接:GPU 不再因为等最慢的请求而浪费算力。实际吞吐量比 static batching 高出很多,队列排队时间也因此缩短——新请求不需要等整个 batch 跑完才能上场。

vLLM、TensorRT-LLM 等现代推理框架基本都采用了 continuous batching。

8. Token Budget

Scheduler 每一轮不能无限制地往 batch 里塞请求。GPU 的算力和显存决定了每轮能处理的 token 数量上限——这就是 token budget。

Token budget 需要区分两类 token:

Prefill tokens:新请求进入 batch 时,整个 prompt 的 token 都要在这一轮处理。一个 2000 token 的 prompt 一次就吃掉 2000 的预算。

Decode tokens:已经在 batch 里的请求,每个每轮只生成 1 个 token。10 个正在 decode 的请求,加起来也就消耗 10 的预算。

所以调度时有一个经典的取舍:让一个长 prompt 的新请求进来做 prefill,会一次占用大量 budget,可能挤掉其他请求的 decode 槽位。或者先让在跑的请求继续 decode,新请求多等一轮。

Token budget 是调度器把"直觉"变成"可执行规则"的关键抽象。有了这个数字,调度策略就能量化地回答"这一轮还能塞多少"。

9. KV Cache 资源池

第五章讲过单个请求的 KV Cache 随上下文增长。在多用户服务里,GPU 上同时有很多活跃请求,每个都需要自己的 KV Cache 空间。

服务端通常把 GPU 显存里用于 KV Cache 的部分做成一个资源池(KV Cache Pool)。新请求进入 batch 时,从池里申请空间;请求生成结束或被取消后,占用的空间归还池子。

池子的容量就是 GPU 上能同时活跃多少请求的硬上限。一个 80GB 显存的 GPU,模型权重占了 30GB,剩下 50GB 给 KV Cache 池。如果每个请求在 4K 上下文下需要 0.5GB cache,那同时最多大约 100 个请求——再多就放不下了。

KV Cache 空间耗尽时,Scheduler 只能选择:等某个请求结束释放空间,或者把某个低优先级请求挂起(preempt)腾出 cache。这就是为什么长上下文和大 batch 之间存在天然矛盾——上下文越长单请求占的 cache 越大,能并发的请求数就越少。

10. Paged KV Cache

传统的 KV Cache 分配方式是给每个请求预留一整块连续显存,大小按最大可能序列长度来。问题是大部分请求用不到最大长度——预留了 32K 的空间,实际只用了 2K,剩下的全浪费了。而且连续分配在请求不断进出的过程中容易产生碎片。

Paged KV Cache(vLLM 提出的 PagedAttention 思路)借鉴了操作系统的虚拟内存管理:把 KV Cache 切成固定大小的小块(blocks / pages),通过一张 page table 记录每个请求持有哪些 block。

请求开始时只分配少量 block,随着序列变长再追加新 block。block 不需要在物理显存上连续——page table 负责维护逻辑到物理的映射。请求结束后,block 归还 free pool,可以被其他请求复用。

收益:显存利用率大幅提升。不再需要按最大长度预留,碎片也大幅减少。同样的 GPU 显存上能同时容纳的请求数更多,吞吐量跟着上去。

这个思路本质就是把操作系统管理内存的经典方法用到了 GPU 显存的 KV Cache 管理上。

11. 优先级与公平性

调度器不只是"谁先来谁先跑"。在真实服务里,不同请求的优先级可能不同,调度需要在效率和公平之间做平衡。

短请求 vs 长请求:短请求占用资源少、跑得快,优先跑短请求可以最大化吞吐量(单位时间完成更多请求)。但如果总是短请求插队,长请求就可能一直排不上——用户体验很差。

VIP vs 普通用户:付费用户、内部关键服务的请求可能需要更高的调度优先级。但如果 VIP 请求永远插队,普通用户的等待时间会变得不可预期。

Preemption(抢占):一种更激进的策略——当高优先级请求到达时,可以暂停低优先级请求(挂起它的 KV Cache 或者保存到 CPU),腾出 GPU 给高优先级请求。被抢占的请求稍后恢复。

具体怎么平衡取决于业务场景和 SLA 设计。调度策略不存在唯一正确解,只有适不适合当前的用户分布和服务目标。

12. 多副本扩容

一块 GPU(一个 worker)能承载的并发请求数有上限。当请求量持续超过单 worker 容量,唯一的办法是加更多 worker——水平扩容。

每个 worker 是一个独立的推理实例:有自己的 GPU、自己的模型副本、自己的 KV Cache 池、自己的 Scheduler。多个 worker 并行工作,每个 worker 处理分配给自己的请求。

扩容的基本单位通常是一个完整的模型副本(replica)。大模型可能需要多块 GPU 才能装下一个副本(tensor parallelism),这时候扩容的粒度就是多 GPU 组成的一个推理节点。

扩容带来线性的吞吐增长,但也带来新的问题:请求怎么分配到不同 worker?各 worker 的负载怎么均衡?这就需要 Router。

13. Router:分配到合适 worker

有了多个 worker,就需要 Router 来做请求分配。

Router 坐在所有 worker 前面,接收进来的请求,然后决定送给哪个 worker。决策依据可以包括:

  • 各 worker 当前的队列长度——优先送给排队最短的。
  • 各 worker 的 KV Cache 剩余空间——优先送给还有空间的。
  • 请求需要的模型——不同 worker 可能加载了不同版本或不同大小的模型。
  • 地理位置——用户在哪个区域,就近路由到最近的节点。

最简单的 Router 就是 round-robin(轮流分配),但效果通常不如负载感知的路由。真实系统会让 worker 定期上报自己的状态(GPU 利用率、cache 占用、队列深度),Router 据此做动态分配。

Router 本身不参与模型推理计算——它的职责纯粹是流量调度。

14. Streaming 与取消

LLM 的生成是逐 token 进行的。Streaming 让服务端每生成一个 token 就立刻通过连接返回给用户,而不是等全部生成完再一次性返回。

用户侧看到的效果就是文字一个一个往外冒——这和 decode 的内部机制天然吻合。Streaming 降低了用户感知的等待时间:虽然完整回答需要几十秒才能生成完,但用户在几百毫秒后就看到了第一个字,阅读速度和生成速度可以并行。

取消也是服务端必须处理的场景。用户可能在生成过程中关闭窗口、切换对话、或者觉得答案方向不对主动取消。服务端收到取消信号后,需要做两件事:停止这个请求的 decode 循环,释放它占用的 KV Cache 空间归还资源池。

如果取消了但不释放资源,KV Cache 池里就会出现"幽灵占用"——没有人在用的空间一直被锁着,其他请求排队等空间。及时回收是服务稳定性的基本要求。

15. 推理服务指标

跑起来以后,怎么知道服务健不健康、用户体验好不好?靠指标。

TTFT(Time To First Token):用户发出请求到收到第一个输出 token 的时间。包含排队等待 + prefill 计算。TTFT 直接决定用户"模型反应快不快"的第一印象。

TPOT(Time Per Output Token):第一个 token 之后,每个后续 token 的平均生成时间。TPOT 决定生成过程流不流畅——TPOT 大了,用户会觉得"字出得好慢"。

Throughput(吞吐量):单位时间内系统完成的 token 数或请求数。从运营角度看,吞吐量决定了同样的 GPU 成本能服务多少用户。

Queue Time(排队时间):请求在队列里等了多久才开始执行。Queue time 高说明系统容量不足或调度不合理。

GPU Cache Usage(缓存利用率):KV Cache 池被占用了多少。接近 100% 时,新请求很难进入 batch,排队时间会急剧上升。

这些指标之间互相关联。比如提高 batch size 能提升吞吐量,但可能增加单个请求的 TPOT。扩容能降低 queue time,但增加硬件成本。服务运维要做的就是在这些指标之间找到平衡点。

16. 服务化总览图

把两章串起来。从用户发出一条消息到收到完整回答,走完了两层系统:

第五章是请求内部的推理链路:

Prompt → Prefill → KV Cache → Decode Loop → Output Tokens

第六章是请求外部的服务链路:

Users → API Gateway → Admission Control → Router → Queue → Scheduler → Batch → GPU Workers → Streaming Response

两层嵌套在一起:服务链路决定了请求什么时候进 GPU、和谁一起跑、分到哪个 worker;推理链路决定了进了 GPU 以后怎么算、缓存怎么管、显存怎么分。

服务端还有几个横切关注点贯穿始终:

  • KV Cache Pool + Paged KV Cache:管理所有活跃请求的缓存空间。

  • Token Budget:每轮调度的资源上限。

  • Continuous Batching:让 batch 动态变化、提高利用率。

  • Metrics Dashboard:TTFT、TPOT、Throughput、Queue Time——把用户体验和系统健康都量化出来。

17. 结语

你在聊天框打一句话发出去,到第一个字出现在屏幕上——这中间发生了什么,现在你应该能说清楚了吧?

文字被切成 token,token 查出向量,向量带上位置进 Transformer,在一层又一层的 attention 和 FFN 里不断更新,最后一个位置的表示经过 LM Head 打出全词表分数,sampling 选出下一个 token。这个循环一步步跑,每次多一个字。

这背后有 prefill 和 decode 的计算形态差异,有 KV Cache 在显存里安静地积累,有 GQA 减掉多余的 head,有 FlashAttention 把 HBM 读写压下去,有量化把模型体积缩进更小的空间。再往外一层,是队列、调度、continuous batching、KV Cache 资源池、router——把一个人用的模型变成很多人同时用的服务。

看起来是魔法,拆开以后都是很朴素的工程决策:这一步计算量有多少,显存够不够,带宽是不是瓶颈,怎么让 GPU 每一秒都不空转。

希望这个课程给你的AI infra之旅,开了个好头!

码字不易,欢迎大家点赞,关注,评论,谢谢!