
1. 推理和训练为什么必须分家从资源特性说起先说一个我自己踩过的坑。早两年做算法平台团队里大家的惯性思维是训练那套Infra这么成熟推理直接套过来不就完事了吗。于是我们把模型权重加载到GPU上写个Flask服务包装一下请求进来就forward一把看起来完全没毛病。结果线上流量一上来GPU利用率只有20%多但每个请求都要等三五秒长一点的文档任务甚至能等到半分钟客户投诉电话直接被打爆。问题出在哪推理和训练在资源特性上根本不是一回事。训练是高吞吐、大batch、可容忍延迟的批量计算。你把几千条样本攒在一起GPU一次forward吃掉一大批数据算得越久越划算只要最终能出loss更新权重就行。它的优化目标是单位时间处理多少样本延迟高几秒、几分钟根本无所谓。推理是低延迟、动态batch、反复交互的在线服务。用户发一句话系统必须在几百毫秒到几秒内给出响应而且请求是持续涌入的每个请求长度不一样、生成的目标长度也不一样。你没法像训练那样把所有请求攒够了再一口气算因为用户等不起。这就像一个是中央厨房做预制菜一个是高端餐馆现点现做前者追求批量效率后者追求的是每桌客人的出餐速度和服务质量。再往深一层MaaSModel as a Service这个模式把推理的压力放大了。MaaS本质上是把模型推理封装成标准化的服务能力对外出售用户不关心你底层跑了什么模型、用了几张卡只关心你的接口快不快、稳不稳、贵不贵。这对Infra提出了几个训练时代根本没怎么想过的要求第一个是多租户隔离与公平调度。一个在线推理服务同时服务千百家客户有人发短文本有人发长篇文档有人做高并发问答如果某个客户的大请求把GPU霸占了其他客户全部被阻塞那SLA就崩了。你得在设计上用调度策略把这个一锅端的风险隔离掉。第二个是成本敏感度极高。训练贵有贵的道理模型效果好老板咬牙也就买了。推理是持续运行的每个token都在烧钱一个热门的7B模型服务一天跑下来电费、折旧、运维成本都是真金白银。系统的吞吐效率直接决定毛利率同样的GPU资源能不能多接住一倍的用户就是盈利能力的分水岭。第三个是稳定性要求完全是另一个量级。训练任务挂了可以重新跑推理服务挂了客户立刻流失。MaaS平台需要一个完整的链路来保证跨请求、跨实例、跨集群的稳定路由、熔断、限流、降级、灰度发布、弹性扩缩容一样都不能少。所以在AI Infra for 推理(MaaS)这个话题上第一性原理就是别把推理当成训练的简化版它是一个独立的系统工程。理解了这一点后面所有模块的设计逻辑才能串起来。2. 一条推理请求的一生拆解MaaS推理链路的核心模块要给推理Infra画结构图最好的办法是跟着一个请求从进来到出去走一遍看它经过哪些关键节点每个节点在干什么瓶颈在哪这也是我梳理架构时最常用的思路。抛开具体产品差异一个典型的MaaS在线推理请求大致会经历下面这些阶段请求网关层。用户把HTTP/GRPC请求打到平台上先经过网关做基础的事情鉴权、限流、路由、协议转换。限流算法可以用令牌桶或者基于QPS的滑动窗口防止某个突发流量把后端打爆。路由则是决定这个请求交给哪个模型服务——是Qwen还是Llama是7B还是72B是CPU推理还是GPU推理这个映射表需要灵活配置往往还要支持按用户级别、按模型版本灰度路由。上下文预处理层。请求里的prompt文本要先被tokenizer转换成token序列可能要做system prompt的拼接、多轮对话历史的组装、长度检查。别看这一层简单它直接影响生成质量比如不同模型对chat模板的格式极其敏感少一个|im_start|可能输出就会乱掉。推理引擎层。这是整个Infra的核心位置。引擎要做几件事把token序列送给模型做前向计算第一个阶段叫prefill预填充把整个输入上下文一次性算完生成首个输出token然后进入decode解码阶段每个token都要把上一轮新生成的token加入序列再做一次前向推理逐步生成后面的token。这两个阶段的特点差异巨大prefill是计算密集型的decode是对延迟敏感且每步只算一个token这两个阶段的资源分配和优化策略完全不同后面我会专门展开讲。采样与解码策略层。模型输出的是一个概率分布需要按你指定的策略来选具体的token贪心解码、top-k/top-p采样、温度系数、beam search、repetition penalty等。采样参数不光影响质量还会影响吞吐——beam search的开销远高于贪心解码实际服务中这都是要按场景做组合配置的。输出与反馈层。生成结束后做tokenizer解码、按需做后处理剔除特殊token、过滤敏感词、计费上报、日志落盘、指标采集最后把文本返回给调用方。如果是流式输出这是MaaS最常见的交互方式引擎还需要支持SSEServer-Sent Events或WebSocket分chunk把结果一个个推给客户端而不是等全部生成完再吐。这条链路上最容易被人忽略的不是单个模块的性能而是模块之间的节奏匹配。网关限定了并发上限但如果引擎端的调度策略和显存规划没跟上网关放进来的请求到了引擎还是得排队。就像餐厅门口排号系统放了一批客人进去结果后厨没有那么多灶台客人全堆在过道里体验只会更糟糕。另一个容易踩坑的点是prefill和decode混在一起会导致长尾效应特别明显。我自己实测过如果不对prefill做拆分控制一个超长文档的prefill请求进来它会占用整块GPU计算资源好几百毫秒甚至几秒这段时间里其他所有请求的decode都被堵住线上的P99延迟直接飙升到几十秒。这是推理服务第一个需要在架构层解决的核心矛盾不要让一个heavy的prefill拖垮一批正在解码的轻量请求。3. 连续批处理的高性能原理一个能提升数倍吞吐的机制在上面说的环节里推理引擎层是Infra的重中之重而谈到现代推理引擎就绕不开一个名词连续批处理Continuous Batching也有人叫它动态批处理Dynamic Batching或in-flight batching。可以说这项机制是让推理服务从能用走向高效的分水岭。传统做法是很朴素的静态批次拼接。假设你的服务最大支持一次处理4个请求就把网络里攒到的请求攒满4个再一起送进GPU这4个请求中任何一个生成完了整个batch都要停下来等最慢的那个也生成完然后一起释放显存、整体换下一批。这就像一条流水线一个工位卡住了整条线都得停着等。这个做法最大的问题是请求的生成长度完全不同。有人问你好只需要生成10个token有人要求写一篇800字的文章要生成1000个token让它们在一个batch里同步进退快的请求必须陪着慢的请求走完全程。这段时间里GPU明明有空闲算力却因为机制限制没法利用起来。连续批处理的核心思路是不要整批同进同出谁完成了谁立刻退出新请求随即补位。引擎在每个解码步骤都做一次调度检查哪些序列生成了结束符或达到最大长度就把它们从显存中释放掉空出来的显存立即分配给正在排队的请求继续迭代计算。这样一批里同时存在的都是尚未完成的序列GPU的有效吞吐被拉到非常高的水平。举一个我实际估算过的例子。我用一个7B模型在单张A100上部署如果走静态批处理即使batch size设到8因为慢请求拖着快请求实际有效并发吞吐大约只有每分钟800个左右的短请求切到连续批处理之后虽然单次请求的端到端延迟没有明显变短但同样的GPU每分钟能处理2500到3000个短请求比之前翻了3倍以上。这就是为什么所有头部推理框架vLLM、TensorRT-LLM、PyTorch Serve等都把它当作默认机制的原因。要自己理解这套机制最好的学习材料其实是nano-vllm。它是vLLM的精简教学版代码量比正经vLLM少两个数量级但你可以在里面清楚地看到Continuous Batching是怎么逐帧跑起来的。我当时跟着它的源码走了一遍几个关键的实现点印象非常深调度器是独立的组件。它维护一个待处理队列和一个正在处理的序列集合每个step都重新决策哪些序列进、哪些序列出。序列状态用step数来区分。每个序列记录自己已经生成了多少token调度器用这些信息来决定它是否满足退出条件。padding mask的处理是性能细节。因为每个step序列长度都在变化GPU计算时通常还是按最大序列长度做padding长度对齐的策略直接决定MEMORY bandwidth的浪费程度。在工程落地时还有一个容易被忽略的问题连续批处理的收益和并发请求数强相关。如果同时在线请求只有个位数连续批处理的效果并不明显反之如果请求堆积很严重它的价值就完全体现出来了。所以做容量规划时不要只看峰值延迟或者平均吞吐还要观察队列深度这个指标才能真正暴露调度器的压力。顺带说一句很多人以为提升推理吞吐就是靠加大batch size但在显存固定的情况下batch size能开多大其实是由每个请求的KV Cache占用决定的这就自然引出了下一个话题——KV Cache的管理。4. KV Cache就是推理的隐形命门显存管理、PagedAttention与长序列如果说连续批处理决定的是调度效率那么KV Cache就是决定物理能装下多少并发的硬约束。先解释一下KV Cache为什么存在。Transformer模型在推理decode阶段每生成一个新token都需要让模型重新回顾整个上下文的Key和Value。如果不做缓存每生成一个token就要全量重新计算所有历史token的Key和Value计算量随着生成长度线性爆炸完全不可接受。所以引擎会把每一步计算出来的Key矩阵和Value矩阵缓存下来后续step直接读取历史缓存只需要处理新增的token。这套缓存就是KV Cache。问题在于KV Cache的占用极其惊人。以一个7B模型、fp16精度为例模型权重大概是14GB左右已经很占显存了但KV Cache的占用同样不可小觑。如果模型的层数、注意力头数、隐层维度配置比较典型每个token大约需要几百KB到1MB的显存来存它的Key和Value具体数值取决于模型结构和精度而一个长文档请求动辄有几千上万个token光一个请求的KV Cache就可能吃下几个GB。多个请求并发时显存分分钟被打满。更麻烦的是KV Cache的动态性。训练阶段显存占用是静态的模型多大就是多大计算前就能算好。推理阶段每个请求的KV Cache大小是随着它一步步生成token而随时变化的你没法提前精确得知用户最终会生成多少个token。如果预留多了显存浪费能承接的并发请求变少如果预留少了生成到一半显存不够直接OOM服务崩溃。vLLM团队提出的PagedAttention就是这个问题的经典解法。它的想法借鉴了操作系统虚拟内存的分页机制把KV Cache按固定大小的block页来管理比如每个block能装下16个token的Key和Value。每个序列不需要在显存里占用一段连续空间而是按需分配若干个block通过block table页表把逻辑上的连续序列映射到物理上分散的block上。序列继续生成时像按需调页一样动态追加block序列结束时整个block链条被完整回收碎片化问题大幅缓解。这个设计最值钱的地方在于显存利用率被提高到了极致。以前静态预分配动不动浪费40%的一半显存用分页之后物理显存几乎每个字节都在为真实的token服务。另外block如果分配得够细还能支持多个序列共享同一个block的KV比如做并行采样时同一个prompt在多个分支上复用前面的KV Cache省显存效果进一步放大。我在实际用vLLM部署服务时对KV Cache的认知是被一个真实故障狠狠教育的。当时我把--gpu-memory-utilization设到0.95想着显存别浪费结果模型上线后经常过一两个小时就报KV cache size out of capacity。后来排查才发现问题出在这个参数控制的是给模型权重 KV Cache总共预留的显存比例但超出了之后引擎并不自动驱逐或换出请求而是直接报错拒绝服务。所以现在我的习惯是这个值最多设到0.85到0.9宁可牺牲一点点显存利用率也要留出安全余量来应对峰值突发。还有一个小技巧如果是对话场景KV Cache的优化绝对绕不开前缀复用prefix caching。系统提示词和对话历史在很多请求里高度重合如果能把这些公共前缀的KV Cache缓存起来直接复用长文档、多轮对话场景的首token延迟可以砍掉一大半。vLLM后来把Prefix Caching做得比较成熟了它会在block级别记录哪些block是跨请求共享的新来请求的prompt如果命中已有前缀就直接映射过去不用重新计算。说回到结构梳理这件事上。KV Cache管理能力直接决定了一个推理引擎能不能支撑MaaS平台的规模化运营。如果底层这层做不好上面无论怎么做策略优化都是空中楼阁反正显存一满所有请求都进不来。5. 量化与投机采样追求吞吐时不能牺牲掉的关键环节在Infra的结构里除了引擎和显存管理还有两类策略层面的手段经常被拿来优化推理性能模型量化和投机解码。这两块都是典型的收益大、坑也深的领域值得单独拎出来说。量化这个事儿很多人的认知停留在把fp16变成int8或者int4就能省显存。方向没错但落地的时候细节特别多。如果你只是在框架层面改了数据精度而没有匹配的量化kernel支持往往会出现显存确实下来了但推理速度反而变慢的诡异情况——因为反量化操作本身是有开销的。常见的量化粒度有两大类一是仅权重量化weight-only比如常见的AWQ、GPTQ把模型权重压到4bit但激活值仍是fp16计算时反量化回fp16做矩阵乘二是权重激活一起量化W8A8权重和激活全部压到8bit直接走INT8矩阵乘来提速。对不同模型、不同任务这两条路线的效果差异很大。我在一个72B模型的服务上试过W4A16的AWQ能把显存占用压到原来的一半不到但真正决定能部署在哪张卡上的永远是显存墙而决定用户体感的则是首Token延迟和吞吐这俩指标不一定跟量化位宽线性相关。实操中我的经验是不要迷信4bit一定比8bit好。量化位宽越低模型质量的损失就越大尤其对数学推理、代码生成这类需要精确逻辑的任务低比特量化经常会出现回答质量明显下降的情况。最稳妥的路径是跑完量化后必须做一组标准评测比对用固定的评测集对比量化前后的通过率、困惑度等指标差太多就升档位。投机采样speculative decoding是另一个有意思的优化方向。它的核心思路非常反直觉既然大模型每个token的解码都慢那我能不能先用一个小模型draft model快速草拟出接下来几个token再让大模型一次性并行验证这几个token是否正确。如果验证通过等于一次前向算出了好几个token解码速度直接翻倍如果某一步验证失败就在出错位置回滚并从那里重新生成。这个方案在batch size较小、延迟敏感的场景里收益特别明显实测常常能让生成速度提升1.5到2倍甚至更多主要取决于草稿模型的命中率。但它在MaaS服务里并没有被普遍用起来原因也很现实需要同时加载一个草稿模型占一份额外显存对小批量请求红利大但大批量场景里大模型的连续批处理已经让GPU算力很满草稿模型反而容易变成负担。另外投机采样对采样参数的语义兼容性要仔细验证——如果你改了temperature或者top-pdraft和verify的一致性逻辑可能会出问题生成结果的分布是否和原来一致也是个需要专门评测的问题。放在Infra的全局视角里量化和投机采样都属于优化手段工具箱里的选项不应该把它们当作默认开启的标准配置。一个成熟的推理平台应该按模型、按场景、按SLA级别灵活组合这些能力。比如低延迟的在线对话用投机采样来压延迟离线批量生成任务就直接跑连续批处理吃满吞吐长文档处理用int8量化来扩大并发容量——配置得越细整个集群的效率才越接近最优。6. 部署与服务化落地路由策略、弹性伸缩和观测指标结构梳理到这一步已经覆盖了推理引擎的核心机制但真正把这些能力变成线上稳定服务的是部署和服务化这层工程能力。这也是很多团队引擎跑通了但线上天天报警的根源所在。先看路由和调度。MaaS平台往往同时在线上服务着多个模型而且每个模型通常有不止一个副本。请求进来时路由层需要做几个决策去哪个模型、去哪个集群、去哪个GPU实例。模型选择相对直观难的是实例选择。两个常见问题一是实例之间的负载不均有的GPU已经排队排到几十条有的GPU还闲着简单地按固定权重轮询就很容易翻车二是亲和性损失同一个用户在多轮对话场景下最好能连续落在同一副本上因为连续对话的KV Cache可能可以复用——如果不做亲和性路由每次把请求打给不同副本前缀缓存的效果就白费了。弹性伸缩这块MaaS场景和传统Web服务有本质区别。传统Web服务是CPU/内存指标起来就扩容一两个实例拉起就开始接流量推理服务扩容涉及模型权重的加载动辄需要几十秒钟甚至几分钟尤其大模型而且单实例能承受的算力是有限的GPU本身就是瓶颈。所以推理场景的弹性伸缩必须做提前量和预测式不能等指标报警了再扩。我见过的方案是基于请求队列深度设置多级水位线水位线达到60%就开始预热扩容90%就强制扩容缩容同理要确保运行中的请求走完再回收实例否则在线请求直接被掐断。MaaS的计费与算力规划也离不开观测数据治理。没有精细化的指标就没法算明白成本和做容量规划。我自己在搭建观测体系时最关注下面这几个指标指标含义有什么用TTFT首Token生成时间最直观的用户体感暴露prefill和排队问题TPOT每个Token的生成时间衡量decode阶段的速度反映引擎效率ITL流式输出中相邻Token间延迟体验类指标换行卡顿就是它的事吞吐Tokens/s单位时间生成的Token总数算成本和机器利用率的核心队列深度等待调度的请求数观察调度器压力的早期信号KV Cache利用率已用显存/总显存判断容量是否充足、是否该扩容我一直觉得队列深度比延迟指标更早反映系统的健康状态。延迟是结果指标等它飙上去再处理已经太晚了队列深度是过程指标它一上来就意味着积压正在形成马上扩容或者降级都来得及。部署层面还有一个容易翻车的点多模型共置。为了节省显存有人喜欢把多个小模型放在同一张卡上理论上可行但不同模型的推理模式不同一个模型出现长文生成高峰时另一个模型的延迟也会被拉爆。如果非要做共置建议在调度器层面把不同模型的执行分组错开或者干脆启用了推理框架的queue机制让不同模型走独立的优先级队列。最后再谈谈成本。推理服务的成本大头是三块GPU机的折旧和电费、运维的人力和带宽成本。GPU成本通常占80%以上所以成本优化的主战场在于能不能更充分地利用每张卡的FLOPS和显存。连续批处理、量化、prefill/decode分离调度这套组合拳下来通常能把单位Token成本降一半左右但降下来之后还有一步不能省——持续的回归评测。任何一次优化改动都要跑一遍功能回归保证模型输出质量没退化同时用压测数据验证延迟和吞吐指标。缺了这道工序优化带来的收益很可能在线上被质量问题全部吃掉。7. 结束语梳理完整个推理Infra的结构我自己的感受是这个领域的复杂度远比想象中高它不是单点技术的堆叠而是调度、显存、网络、容灾、观测、成本多个维度耦合在一起的系统工程。而且有一点和训练Infra很不一样训练系统每一次优化容错空间很大推理系统每一次改动都会直接暴露给终端用户所以上线前的回归验证和监控设计千万不能省。如果是从零开始搭建或者学习这个方向我个人的建议是先不要一头扎进复杂的框架源码里而是从nano-vllm这类小型实现入手理解请求是怎么被调度、KV Cache是怎么被管理、连续批处理是怎么运作的这几条主线然后回到vLLM这样的生产级框架里对照验证。实际部署时从一个小模型、两个副本开始把指标观测和故障恢复跑顺了再往上加规模。推理Infra的很多坑都是用肉眼盯出来的不自己压一次测、不自己看着队列深度飙升一次很难真正理解那些参数设计背后的理由。