群发资讯网

推理芯片系列文章四:一座 token 工厂是怎么运转的

上两篇把账算到了单位 token:一百万 token 多少钱,取决于全口径成本,也取决于有效交付。这一篇解释那个“有效”是怎么来的——同一批机器,为什么能跑出完全不同的产量。

一块推理芯片不会自己生产 token。它需要请求持续进来,需要调度器把长短不一的任务拼成一批,需要 KV Cache 存住历史,需要网络把多块芯片连成一台机器,还需要某块卡出故障的时候继续服务。

换个说法,推理系统是一座工厂:网关接单,tokenizer 把文字切成机器能读的编号,调度器排队拼单,Prefill 读题,Decode 写字,KV Cache 是在制品仓库。芯片再强,这条线排不好队、仓库爆了、网络堵住,单位成本照样下不来。

图|一个请求怎样变成 token:从 API 入口到流式返回

一、一次请求,走完八个工位

一次请求不是交给芯片就完事。它要过接入、鉴权、切 token、路由、组批,再进 Prefill 和 Decode,最后流式推回手机。八个工位,每一站都可能加延迟,也都可能把算力白白烧掉。

接入和鉴权。请求先进 API 网关,密钥、配额、租户级别、内容大小、并发限制在这一层检查。看着跟 AI 计算没关系,实际决定后面稳不稳。网关不限超长提示、不限突发并发,几条请求就能把后端挤爆。

tokenizer。模型不读汉字,只吃 token ID。它把文本切成编号,切出多少 token,直接决定计费、上下文长度、KV Cache 大小和后面的调度压力。同一段文字,不同 tokenizer 切出来的数量可以不一样。这不是一个能顺手带过的步骤。

admission 和路由。系统按模型、输入长度、预计输出、租户优先级、可用显存和队列状态,把请求分进不同的服务池。这个决定很硬:短问答、长文档、长链路 Agent 全塞进一个池,一条超长 Prefill 就能把正在出字的短请求堵在门口。

scheduler 组批。它像车站调度员。车太小,车次多、运力浪费;车太大,乘客得等着凑够人。batch 大能把权重读取摊薄,但等待变长,长短请求混在一起还会互相拖。

Prefill。模型读完整个输入,逐层算完,生成第一个 token,同时把历史的 Key 和 Value 写进 KV Cache。

Decode。每轮给每个活跃请求出一个 token。请求结束、撞上停止词、到达长度上限,就让出位置,新请求补进来。

流式返回。token ID 经 detokenizer 变回文字,过安全、格式、工具调用处理,一段段推给用户。

计量和观测。输入、输出、缓存命中、排队时长、TTFT、ITL、错误、KV 占用、硬件功耗,全部记下来。没有这些数,算不出真实成本,也看不见瓶颈长在哪儿。

这八步不是一条只往前走的直线。用户一取消,KV 要从 Decode 反向回收;worker 一挂,请求退回路由层重来;工具调用把中间结果拼回提示,再走一遍 tokenizer、Prefill 和 Decode。Agent 表面上问了一次,内部可能穿过整条线好几趟。

二、三种等待方式

图|三种调度,三种等待方式

最朴素的排法是静态批处理:等一批请求凑齐,一起跑到全部结束。

麻烦在长度不一样。短请求早该走了,还得陪着长请求熬;长请求占着位置,形状不整齐还会在计算阵列里留下空洞。

大模型的 Decode 本来就是一轮一轮走的。连续批处理把调度粒度缩到生成迭代上:哪个请求一完成,下一轮立刻补新的进来。

设备利用率上去了,调度器要背的状态也多了一截。每条序列的长度、KV 位置、采样参数、结束条件都不一样,每一轮都得重新组织。

加并发通常能摊薄权重读取,总 token/s 往上走。代价是请求得等位置,单用户的 TTFT 和 ITL 都会变差。生产系统要的不是最大吞吐,是尾延迟压得住的最大 goodput。

连续批处理不是把请求 ID 塞进队列就算完。调度器至少得记住:请求在哪个阶段、已经处理了多少 token、输出上限、采样参数、停止条件、KV 块表、前缀缓存引用、优先级、租户配额、有没有被抢占。

每轮 Decode 之前,它要回答四个问题:哪些序列继续跑;哪些新 Prefill 能塞进来;显存不够时先抢谁;这一批 token 的形状怎么拼,内核才跑得顺。

答错一次,轻则吞吐掉,重则 KV 指向错位、序列串扰、请求收不回来。

PagedAttention 那篇论文回顾过迭代级调度和块表映射。它解决的是显存分配和共享,不负责替你满足延迟目标。这两件事经常被混着说。

三、长 Prefill 为什么会堵住 Decode

Prefill 一次要把整段输入算完。突然进来一条超长提示,这一次计算能持续很久。

同一时刻,已经在 Decode 的人在等下一个字。两类任务共用一个执行队列,就会出现 generation stall:输出忽然停住,用户以为网络卡了。

Chunked Prefill 的办法是把长提示切成块,一次只算一部分,块与块之间插进 Decode。

块太大,阻塞还在;块太小,调度、同步和内核启动的开销反而涨上来。最佳块大小看硬件、模型、输入分布和 ITL 目标,没有固定值。

一次迭代的耗时,大概由四块构成:

迭代时间 = Decode 计算时间 + 本轮 Prefill 分块时间 + 调度与内核启动时间 + 通信等待时间

如果用户希望每 50 毫秒看到一个新字,这四项加起来不仅平均要低于 50 毫秒,P95、P99 也得守住。分块开大,Prefill 那项超标;分块开小,调度和启动两项把总数顶上去。

Sarathi-Serve 做的正是围绕这种干扰来切块调度;DistServe 走得更远一步,直接把两个阶段拆到不同资源池。

四、KV Cache 是在制品仓库

图|KV Cache 为什么要分页:逻辑连续,不要求物理连续

KV Cache 不像“缓存”两个字听上去那么轻。它存的是模型已经读过的上下文状态,往后每生成一个 token,都得接着读写它。上下文越长、并发越高,这个仓库越容易在 HBM 里撑满。很多系统的瓶颈最后都落在这一层。

为什么要存。生成第 1001 个 token 时,模型得看前 1000 个。每轮都从头重算一遍历史,代价没法接受。KV Cache 把各层历史的 Key 和 Value 存下来,新一轮只算新 token,其余直接读缓存做注意力。

连续预留为什么浪费。按每条请求的最大上下文一次性预留一块连续显存,大部分请求会提前结束,留下用不上的空洞;长度混杂时还会碎。账面剩余空间明明不少,就是找不到一块够大的连续区域。

PagedAttention 怎么处理。把一条逻辑序列的 KV 切成固定大小的块,映射到不连续的物理显存。请求要长就申请新块,结束就释放;共享前缀直接引用同一批物理块。

它提高了显存利用率,但没有把 KV 变没。块表管理、引用计数、缓存一致性、访问跳转,都是新加上来的成本。

落到物理层面:一条序列一张 block table,逻辑第 i 块先查表拿到物理块号,再定位每一层、每个 KV head 的数据。复制会话或者 beam search 时,多个序列可以共享只读的前缀块;一旦某条分支要继续写,就得新开块,必要时做写时复制。

块太大,序列尾部留着内部碎片;块太小,块表更长,地址转换和调度元数据更多。系统还得防引用计数没归零导致的泄漏,也要防提前回收让还在跑的 kernel 读到已失效的数据。

prefix cache 什么时候有效。固定系统提示、共享文档、同一个代码仓、多轮对话的前缀,都适合复用。前缀各不相同,或者权限隔离不允许跨租户共享,命中率就上不去。缓存能省多少,只能用真实流量测。

HBM 不够以后。可以把一部分 KV 挪到主机内存、远端内存或者独立缓存节点。容量是大了,但每次搬运都要过 PCIe、CXL 或者网络。传输跟不上 Decode,缓存分层就从解法变成新瓶颈。

值不值得挪,先算传输下界:

KV 迁移时间下界 = KV 字节数 ÷ 端到端有效带宽 + 协议、排队与交换跳数延迟

一条长请求要迁 20GB KV,端到端有效带宽 100GB/s,光搬数据就是 200 毫秒,排队和软件开销还没算。这个例子足以说明,“网络标称 400G”推不出迁移够快:链路编码、协议、共享、拓扑、收发端的内存带宽,每一项都会把有效带宽压下去。

五、把 Prefill 和 Decode 拆开

图|P/D 解耦把网络放进关键路径

Prefill 和 Decode 的形态不一样。一个像集中读题,一个像连续写字。拆开,是想让适合读题的机器专心读题,适合写字的机器专心写字。拆完会多出网络传输和调度复杂度,所以不是任何场景都稳赚。

Prefill 矩阵大,更容易被算力卡住;Decode 矩阵窄,更容易被 HBM 带宽、KV 和通信卡住。两类任务挤在同一批机器上,配置只能折中。P/D 解耦把它们放进两个池:Prefill 池负责读输入、产 KV,Decode 池负责持续出字。

拆分后的完整链路是这样的:路由把新请求送到 Prefill worker;Prefill 算完,登记 KV 的位置和元数据;KV 经高速网络发给目标 Decode worker,或者放进双方都能访问的缓存池;Decode worker 接管序列开始生成;请求结束,回收 KV 和路由状态。

拆分为什么可能更快。两个池能用不同的机器比例、batch、并行和调度策略。长输入变多就加 Prefill 容量,输出变长就加 Decode 容量。

拆分为什么也可能更慢。KV 很大。传输、注册、拓扑绕行、目标 worker 排队,每一项都加时间。比例估错,一个池排长队,另一个池闲着;路由器不看物理拓扑,KV 会跨好几层交换;缓存节点一挂,请求可能得重算一遍。

所以 P/D 拆分不是把两个进程分开就完事。它把网络和缓存变成了执行的一部分。

DistServe 把性能目标写成满足 TTFT 与 TPOT 约束的 goodput,还专门分析了共置带来的干扰;Splitwise 分 prompt machine 和 token machine;Mooncake 把 KV Cache 当成独立资源,做缓存感知路由和弹性 Prefill 池。三条路线说的是同一件事:拆分不是一个开关,是一组容量规划、路由和故障恢复机制。

六、多卡之后,问题变成通信

并行方式

切什么

代价在哪

数据并行

请求

每个副本占一整份权重内存

张量并行

一层矩阵

每层都要 all-reduce 或 all-gather,吃 scale-up 互联

流水并行

层

微批不够、阶段不均就出气泡

专家并行

专家

all-to-all,热点专家拥塞、其余卡空转

上下文并行

序列

attention 要跨卡交换,长上下文下代价陡增

数据并行最省事:每组卡存一份完整模型,各处理各的请求。代价是每个副本都占一整份权重内存。

张量并行把一层矩阵切到多张卡上,每一层都可能要 all-reduce 或者 all-gather,对 scale-up 互联的延迟和带宽要求很高。

流水并行把不同层放到不同卡上,请求像流水线一样往下走。微批不够、阶段不均衡,就会出现流水气泡,卡在那儿等上一段。

专家并行最麻烦。MoE 的专家散在不同卡上,router 先决定每个 token 去哪几个专家,再靠 all-to-all 把 token 发过去。大量 token 同时选中少数专家,那几张卡拥塞,其余卡闲着。负载均衡、token 丢弃、冗余专家、网络拓扑,每一项都影响效率。

一次 MoE 层通常这么走:router → top-k → token 重排 → all-to-all → 专家 GEMM → all-to-all → 恢复顺序。通信量不只由 token 数决定,还看隐藏维度、激活位宽、top-k 和跨节点比例。平均每卡 token 数一样,也可能因为某一轮热点专家集中在远端,冒出尾延迟。

上下文并行沿序列维切超长上下文,代价是 attention 要跨卡交换信息。上下文越长,通信和 KV 管理越吃重。

诊断的时候账要分开记:平均负载和最大负载分开,字节数和消息数分开,卡内、机内、机架内、跨机架分开。小消息多受延迟限制,大消息多受带宽限制,把这些合成一个“网络利用率”,等于什么都没看出来。

七、投机解码:少跑几轮

Decode 慢的一个重要原因,是每轮只确认一个 token。

投机解码让一个更小的草稿模型先提几个候选,再交给目标模型并行验证。候选大多被接受,目标模型一次前向就能确认好几个 token,轮数直接少一截。

收益全看接受率。任务越难、采样越随机,草稿越容易猜错;草稿模型自己也占算力、显存和调度位置。

所以真正要比的是:省下的目标模型轮数,减去草稿、验证、回滚和 KV 同步的成本。只看一个“接受率 80%”,看不出这笔账是赚是亏。

八、怎么看出问题,怎么不骗自己

图|从症状反推推理瓶颈

离线和线上是两回事。模型离线评测全对,不代表高并发下不出怪错。队列积压、KV 块耗尽、P/D 比例失衡、缓存迁移中断、通信超时、版本不匹配、抢占恢复,最后都可能表现成异常字符、重复、输出停顿或者错误率往上走。

要盯的至少有七类:排队时长和各池队列长度;TTFT、ITL 和 P99;活跃序列、batch 和 token 数;KV 总量、空闲块、抢占和 offload;Prefill、Decode、KV 传输各自的耗时;每种错误、重试和取消的原因;每张卡的算力、带宽、功耗和温度。

定位可以从症状倒推。TTFT 涨而 ITL 正常,先查接入排队、Prefill 池、长提示和前缀命中;TTFT 正常而 ITL 抖,先查 Decode 的 batch、KV 压力、降频和 scale-up 通信;两个一起坏,先看总负载、worker 故障、路由失衡和网络拥塞;OOM 变多但 HBM 平均占用不高,先查碎片、单条超长请求、块泄漏和各卡分布不均;吞吐正常但错误率上来,先查抢占恢复、版本组合、量化精度和流式返回链路。

跑 benchmark 也是同一件事。可信的测试不会只说“我这套卡跑到 3 万 token/s”,它得写清模型、精度、输入输出长度、并发、缓存命中、失败重试和 SLA。否则比的不是芯片,是两套完全不同的测试条件。

至少固定七项:模型和权重版本;量化、KV 精度和精度回归结果;输入输出的长度分布;请求到达分布和并发;硬件、驱动、框架和 serving 版本;预热、测试时长和缓存状态;TTFT、ITL、P99 和错误率门槛。

最后同时报总吞吐和 goodput。只报离线最大 token/s,判断不了用户有没有真的被服务。

还要分清开放环和闭环。闭环客户端收到响应才发下一条,系统一慢,请求率自己就降下来,过载被掩盖了;开放环按预设到达率一直发,更接近真实的突发流量,但得限测试时长和队列深度,防止无界积压。

一场像样的测试要过四段:预热、稳态、过载、恢复。稳态看效率,过载看退化方式,恢复段看队列能不能退、KV 有没有回收、失败请求会不会被重试风暴再放大一遍。只有峰值没有恢复段,判断不了这套系统能不能长期运营。

九、结论

推理服务器从来不是“一张卡加一个模型文件”。它是一套 24 小时跑着的生产系统。

调度器决定计算单元空不空转,KV Cache 决定能同时装多少人,网络决定多卡和 P/D 拆分能扩到多大,观测和恢复决定高负载下那些 token 到底算不算交付。

所以同一款模型、同一批卡,换一套调度策略,单位 token 成本就能变一截。变的不是芯片参数,是生产线有没有让芯片在那儿干等。