开头
干这行久了,有个很深的感受:一说“AI Infra”,大家第一反应是训练集群、万卡调度,但真正到了推理这一步,画风完全变了。训练再牛,模型最终要以服务的形式跑起来才算落地,这中间的工程问题多到让人头皮发麻——显存怎么分、并发怎么控、延迟怎么压、成本怎么算。我最近梳理了一遍自己做MaaS(Model as a Service,模型即服务)推理基础设施的完整结构,把从模型部署到对外提供服务的整条链路拆开来看,发现很多环节比想象中更考验细节。
这篇博文不聊训练的分布式框架那一套,就聚焦在“推理服务化”这件事上,把AI Infra for Inference的结构从上到下捋一遍。适合正在做模型服务化、推理加速、GPU资源池化,或者准备搭建私有化MaaS平台的团队参考。咱就从用户发来一个请求开始,看这个请求在后台到底经历了什么。
1. 内容整体设计与思路拆解
1.1 MaaS的核心矛盾:不是“能推理”,而是“稳定地、低成本地推理”
先说个经常被低估的事实:把一个大模型跑起来,只要一张能用的GPU就行,哪怕慢点也无所谓。但MaaS要面对的问题远不止于此——你接的是外部流量,有高峰有低谷,有海量并发,也有个别又长又复杂的请求把显存吃穿。真实场景下,一个推理服务至少要过三道坎:
- 稳定性:单卡挂了不能全站挂,一个请求超时不能拖垮整个Worker进程。
- 确定性:P99延迟要可控,响应时间抖动不能太大,否则业务方会投诉。
- 成本:GPU利用率上不去,就是纯烧钱。一个token的边际成本算不清楚,整个定价体系都是空中楼阁。
所以我梳理MaaS结构时,习惯把它分成四层来看:接入与网关层、调度与路由层、执行与引擎层、观测与运维层。这四层各干各的活,又相互咬合。模型推理在整个架构里是核心,但绝不是全部。
1.2 基于nano-vllm入门是最务实的路线
关于“如何上手大模型推理”,我见过很多人一上来就啃vLLM源码,结果被PagedAttention和各种Kernel劝退。我的经验是:先跑通nano-vllm(vLLM的精简教学版),再去看生产级代码。nano-vllm把关键路径抽出来了,包括Continuous Batching、KV Cache管理、Prefix Caching的实现逻辑,用很小的代码量说清楚核心机制,非常适合用来建立推理框架的“工程直觉”。
要理解MaaS,推理引擎的运行机制是地基。先搞懂请求、KVCache、显存管理、Batching策略这几个概念,再看网关、调度、弹性伸缩等外层设施,思路就清晰多了。后面我会用大量篇幅讲这些机制。
2. 核心细节解析与实操要点
2.1 推理引擎选型:vLLM、TensorRT-LLM、SGLang 还是自研?
这一节必须先讲清楚,因为整个MaaS的性能上限由推理引擎决定。市面上的引擎我基本都实际部署过,简单说说感受:
| 引擎 | 优势 | 适用场景 | 踩坑点 |
|---|---|---|---|
| vLLM | 生态好、吞吐高、易用性强、社区活跃 | 通用对话、RAG、Function Calling | 长序列场景显存碎片化问题需要调参 |
| TensorRT-LLM | 单卡性能极致,延迟低 | 对时延敏感的线上SLA业务 | 图编译耗时久、灵活性差 |
| SGLang | RadixAttention做Prefix Cache,多轮/多请求复用很猛 | 高并发共享Prompt、Agent类场景 | 新框架迭代快,稳定性要等版本成熟 |
| 自研 | 完全可控、可极致定制算子与调度 | 超大厂且场景极其固定 | 成本极高,非必要不碰 |
选型逻辑很简单:90%的团队直接用vLLM就行,它已经把Continuous Batching、PagedAttention这些核心技术做成开箱即用的方案了。TensorRT-LLM适合那些“硬件型号统一、请求模式稳定、对延迟极度敏感”的纯线上业务。至于自研引擎的前提是——团队有CUDA优化能力,并且业务形态确实超出了开源引擎的优化边界。
2.2 一个请求在引擎内部的完整旅程:从Prefill到Decode
这部分是理解推理Infra的重中之重,所以我会拆得细一点。
假设用户输入“请写一段Python代码计算斐波那契数列”,引擎内部大致发生这些事:
- Tokenization:把输入切成Token序列,变成模型的输入向量。
- Prefill阶段:一次前向计算,把整段输入并行算完,生成首个输出Token,同时产生KV Cache。这个阶段是计算密集型,GPU算力吃满,吞吐高。
- Decode阶段:一个Token一个Token地往后推,每步只输入最近的一个Token,用KV Cache加速上下文计算。这个阶段是访存密集型,瓶颈在显存带宽。
- 流式返回:每生成一个Token就通过流式接口返回给用户,这就是为什么你看到ChatGPT打字是一个字一个字蹦出来的。
为什么KV Cache是命门?因为Decode阶段每生成一个Token,都要把之前所有Token的KV Cache读出来算Attention。序列越长,Cache越大,显存占用涨得越快。一个7B模型,FP16权重只要14GB,但一个1K长度的请求叠加16个并发,KV Cache就能占到好几个GB。这也是为什么我们要引入PagedAttention和显存管理。
实操要点一:显存分配不要贪心很多人以为模型的KV Cache显存池越大越好,实际上留太小会频繁触发换入换出(Swap),留太大又压缩了并发上限。我常用经验值是:KV Cache显存 = 总显存 - 模型权重大小 - 激活值预留(通常1-2GB) - 计算储备。具体用gpu_memory_utilization参数控制在0.85到0.92之间,先跑压测再微调。
2.3 Continuous Batching和PagedAttention:吞吐翻倍的功臣
传统Batching是等一批请求都处理完再开下一批,很蠢——只要有一个长请求没走完,同批的短请求只能干等。Continuous Batching(持续批处理)的做法是:批内任何一个请求生成完毕后,立刻插入新请求。这样GPU始终在干活,不会出现“等最后一个长尾”的情况。
衡量这个逻辑有没有做对,看一个指标——MJ/s(每GPU每秒生成的百万token数)。在长尾请求场景下,Continuous Batching能让吞吐量提升2到3倍,这是MaaS盈利模型能不能成立的核心变量。
PagedAttention则是把KV Cache切成分页(Page)来管理,类似操作系统的虚拟内存分页。不必给每个请求提前分配连续的最大长度空间,而是按页动态分配。缺页时还能从CPU内存换入换出。这让显存利用率大幅上升,vLLM在长序列场景下还能保持稳定。
实操要点二:Prefix Caching别忽略对于RAG系统,用户问的问题前缀经常是固定的系统Prompt(“你是AI助手...”、“请基于以下文档回答...”)。启用Prefix Caching(前缀缓存)后,重复前缀的KV Cache直接跳过Prefill重算,能省下30%-50%的首Token延迟。vLLM里配置enable_prefix_caching=true即可。代价是前缀的LRU淘汰策略要观察,Cache命中和业务问法强相关,别盲目开。
3. 实操过程与核心环节实现
3.1 平台整体架构设计:从网关到GPU池
讲完引擎层,再往上一层看MaaS平台的健壮性。项目里我的典型架构是这样:
Client → API Gateway → Router/Scheduler → Engine Pool (vLLM/TensorRT-LLM Workers) ↓ Metrics/Logging/Monitoring网关层:负责鉴权、限流、计费、请求灰度。这是MaaS的入口,也是商业化的第一道防线。常见做法是OpenResty或Go写的轻量网关,统一处理Token计量。
路由与调度层:这个是AI Infra for推理和普通后端服务最大的分水岭。调度器要懂模型,要知道哪些GPU上跑了哪个模型、剩余显存多少、当前并发多少,然后把新请求送到最优的Worker上。我常用的调度策略有三种:
| 策略 | 逻辑 | 适用场景 |
|---|---|---|
| Round-Robin | 轮询分发 | 同模型多副本,负载均匀 |
| Least-Connections | 按当前请求数最少分发 | 请求长短差异大的场景 |
| Load-Aware | 按GPU利用率/显存余量动态分发 | 异构GPU池、混布场景 |
想省事就直接用KServe或Serving框架自带的Router,但到了规模后,还是得自己写调度器。毕竟只有自己最清楚业务的请求画像。
3.2 GPU显存管理与弹性伸缩:算力资源池化
推理场景下GPU很贵,不能让每个模型固定独占一张卡。我在项目里用到的资源池化方案是:模型分片部署 + 显存超卖 + 弹性收缩。
先说显存超卖。一个模型实际部署时KV Cache是动态增长的,但峰值显存需求并不总是满载。可以在系统层面设置enable_gpu_memory_utilization为一个安全上限,但实际请求并发跑不满,剩下的显存可以临时跑小模型任务。这里要特别小心显存溢出把Worker炸掉,所以需要精细化记账:
- 每个Worker启动时记录基础显存占用(模型权重+静态结构)
- 运行时按请求池状态的增减实时更新剩余显存
- 新任务申请前检查“剩余显存 > 峰值需用 + 20%缓冲”,否则不调度
再说弹性伸缩。MaaS流量潮汐效应非常明显,白天上班高峰、晚上和周末下降。我们做了按请求量自动扩缩容:指标触发为GPU利用率 > 70%持续5分钟扩容一个副本,GPU利用率 < 25%持续30分钟缩容一个副本。缩容前要保证当前Worker上的请求全部排空,否则会有大量超时。
3.3 推理任务的加速细节:从量化到算力换速度
推理加速的手段很多,但有一条原则贯穿始终:延迟和成本不可兼得,必须在业务SLA下做权衡。
量化是降本提效的第一步。从FP16到INT8,显存立减一半,吞吐最高能提升60%-80%。如果对精度要求不那么苛刻的模型,AWQ或GPTQ量化是首选。4-bit量化虽然能进一步压显存,但实测下来某些模型的推理质量会掉,特别是数学、代码类场景,建议先跑一遍评测集再上。
我常用的量化选型参考:
| 模型量级 | 推荐量化 | 显存节省 | 精度损失 |
|---|---|---|---|
| 7B-13B | FP16/INT8 | 0-50% | 几乎没有 |
| 13B-70B | INT8/AWQ | 50%-75% | 可接受 |
| 70B以上 | AWQ/GPTQ 4-bit | 80%-90% | 需业务侧验证 |
还有一个容易忽略的点:Batch Size与模型并发的匹配。推理引擎里的max_num_seqs、max_num_batched_tokens这些参数直接影响吞吐。我在7B模型上实测,max_num_seqs=256时P99延迟能控制在1.5秒内,吞吐比默认配置高45%左右,但显存压力也更大,得对着监控逐步上调。
3.4 Serving框架与API设计细节
一个成熟的MaaS平台,对外API要支持的参数远不止model和prompt。我这边实际开放的能力包括:
- 流式与非流式:所有模型必须支持
stream=true,否则用户感知“打字机效果”就没有了,长文本场景超时风险也大。 - 参数透传:
temperature、top_p、max_tokens这些基础参数要透传到引擎。 - 结构化输出:支持JSON Mode和Function Calling——这是Agent类应用的刚需,底层要用到guided decoding,对延迟有影响。
- 自定义停词:业务方常要求“输出到某个词就停止”,这些做在网关层统一处理,不侵入引擎。
- 排队与超时:高峰期不能让请求直接失败,要有排队机制和明确的超时反馈。我这里的逻辑是:排队超过10秒就拒绝并返回
503,让客户端自动重试。
实操要点三:长连接别全链路开流式接口用WebSocket或SSE时,网关到引擎之间千万注意连接池大小。我踩过一个坑:网关开500个连接,引擎只配了256个并发槽位,结果大量连接处于半开状态,P99直接飙到10秒。后来改成“网关无状态转发 + 引擎从连接池取连接”就好多了。
4. 常见问题与排查技巧实录
4.1 排查问题的方法论:先测引擎,再查平台
我自己调试MaaS平台有一套固定流程,能省下大量时间:
- 先直连引擎测:绕过网关和调度器,用
vllm serve或在代码里直接调用引擎接口,定位问题在引擎还是平台其他层。这一步能在五分钟内排除掉80%的干扰项。 - 再压测网关:用Locust或wrk单独打网关,看会不会引入额外延迟。重点看网关有没有超时截断、重试风暴、连接复用失效。
- 最后观测调度决策:查日志,看请求被调度到哪个Worker、当时的显存和并发快照是否有问题。
- 全程看监控:没有Prometheus+Grafana这套的话,几乎没法排查。必须埋的指标:PV、UV、请求延迟分位(TP50/TP99/TP999)、GPU利用率/显存/温度、KV Cache命中率、排队等待时长、请求拒绝率。
4.2 经典报错与对应解法速查表
| 现象 | 根因 | 解决方式 |
|---|---|---|
| CUDA OOM | KV Cache池过大或并发暴涨 | 降gpu_memory_utilization;限流接入层;检测并发护栏 |
| 首Token延迟飙升 | Prefill计算过长或Prefix Cache未命中 | 开启前缀缓存;限制单请求输入长度;对超长prompt拆分 |
| 长时间卡顿后超时 | Decode阶段单批过长 | 检查max_num_batched_tokens;开启Continuous Batching |
| 生成质量突然下降 | 量化精度受损 | 回退FP16;量化前跑评测集 |
| 多卡设备利用率不均 | 调度策略与模型放置不均匀 | 手动绑定模型到卡;调研设计负载感知调度 |
| Worker内存上涨无法回收 | 长连接泄漏 | 检查连接池生命周期设置;加入max_requests周期性重启Worker |
| 部分请求流式输出中断 | 网络层缓冲导致连接被切断 | 检查代理缓冲区,配置SSE event的flush节拍 |
4.3 一个真实案例分析:混布场景下显存“吸血鬼”
有一次,平台同时部署了三个模型,其中一个是13B的代码模型,另外两个是7B的对话模型。跑着跑着,突然代码模型的P99延迟从1.2秒飙到4.5秒,而且一查GPU显存,代码模型Worker的显存占用一直在涨。
排查过程:先按前面说的方法直连代码模型引擎,延迟正常。回到调度器看日志,发现有一个7B模型的请求被调度到了代码模型所在的卡上,因为这块卡“剩余显存”刚好够。问题出在7B模型压进来的那一刻,把KV Cache池的剩余页挤掉了,代码模型开始频繁做Swap换页,全卡性能骤降。
解法就是严格的“卡级隔离 + 显存超售阈值收紧”:同卡只能跑同优先级的服务模型,异模型混布时剩余显存阈值从20%提高到40%。这个案例再次验证了那句话——调度上的宽松,最终代价都是延迟和稳定性。
5. 资源与成本的量化分析
5.1 推理成本怎么算才准?
很多团队算推理成本只算GPU租赁费,这是不对的。实际成本包含五块:
- 硬件成本:GPU服务器折旧/租赁(大头)
- 电力成本:单卡功耗按300W算,实际机柜还有散热开销
- 工程成本:运维、MLOps、调参的人力分摊
- token成本:输入和输出Token都消耗时间与显存,输出Token更贵(因为Decode慢)
- 失败成本:超时重试、排队等待、坏掉的请求占用的资源
成本计算公式我习惯按“单个请求的边际成本=GPU单卡时成本×请求占用时长”,然后向上取整到“每百万Token成本”做定价。一个典型例子:8卡A100集群跑一个70B模型,月成本约5万(含电力和运维),日均处理1000万输入Token、300万输出Token,摊下来每百万输出Token成本约20元左右。定价低于这个数,就是亏本做慈善了。
5.2 提效与降本的实操经验总结
从实际操作角度,四个最有效的降本动作:
- 模型瘦身:蒸馏+量化双管齐下。能用7B,不跑13B;能用INT8,不上FP16。
- Batch优化:调大
max_num_seqs,把GPU的算力和带宽榨干。前提是显存管得住。 - 负载转移:Prefix Cache的复用率做上去后,大量Prompt直接跳过Prefill计算,成本下降立竿见影。
- 缩容策略:非高峰时段直接停止副本,切到Serverless模式——VLM的镜像能秒级起。
实操要点四:利用率不是越高越好GPU利用率在95%以上看着爽,但这时候延迟曲线通常已经翘尾了。我一般把长稳态目标放在70%-85%。太低是浪费钱,太高是拿延迟换成本效率,业务SLA会出事。
6. 写在最后的一个经验
这些年在推理侧做Infra,心里最深的一个体会是:AI Infra for推理的复杂度是系统的,不是单点的。引擎再快,网关和调度拖后腿,用户体验还是烂;显存算得再精,业务请求一乱,照样OOM。要从整体去看整条链路,哪里是瓶颈就补哪里,而不是迷信某个“神奇参数”。
还有一个不得不提的细节:日志和监控一定从第一天就做。很多平台上线后才发现上线的推理服务像“黑盒”,出了事故只能靠猜。我经历过一个凌晨事故——新版本的推理引擎在某种Transformer结构下会出现偶发死锁,没有完善的消息队列和心跳监控,根本定位不到。后来我们给每个Worker加了心跳和请求级全链路Trace,这类问题基本都能在五分钟内圈定范围。
最后分享一个小技巧:如果你刚开始搭建推理MaaS,不要一上来就追求全功能。先把“单模型、单卡、稳定服务”这条最小闭环跑通,再逐步加多模型、多卡、弹性伸缩、租户隔离。一口吃不成胖子,把地基打扎实了,后面加什么功能都顺。这套结构梳理到这里,希望能帮正在做这个方向的你少走一点弯路。