上一周帮一个朋友处理了个典型事故他在本地把开源模型跑得很欢问答、写作、翻译样样都行于是跟我说“推理这块稳了”。结果一上接口、接并发、做压测三天折腾下来接口不是超时就是显存爆炸被拖得焦头烂额。这个场景其实非常普遍很多人的认知停在“模型能出结果”这一层但一个能在命令行里对话的模型和能被业务系统稳定调用的服务中间隔着一条巨大的工程鸿沟。这一篇就围绕推理系统和服务化的真实距离把模型跑起来以后会遇到的问题一条条拆开讲清楚。先说结论推理系统负责把模型变成计算过程服务化负责把这个过程变成稳定、可扩展、可观测的产品。两者不是同一个维度的问题后者远远复杂于前者。这篇文章适合那些刚把模型跑通、准备上线的工程师也适合正在从“本地调模型”向“生产环境扛流量”过渡的团队。看完你会理解为什么“能跑”和“能服务”之间差了这么多以及真正落地时要抓哪些重点。1. 先搞清楚一件事能跑不等于能服务1.1 “推理系统”和“服务化”到底差在哪先定义清楚两个词不然后面所有讨论都会失真。推理系统指的是从输入数据到模型输出之间的完整计算链路文本预处理、Token化、模型前向计算、后处理、结果返回。它解决的核心问题是“给定一个输入如何正确高效地得到输出”本质上是一个计算过程。服务化则是把这条计算链路搬进真实的业务环境让它以接口的形式对任何调用方开放并且能扛住不确定的流量、满足明确的性能承诺、做到异常可恢复。服务化要处理的不再是“怎么算”而是“在多少人同时算、算多久、出错怎么办、不够用怎么办”这些工程问题。这个差异可以类比成开餐厅推理是一个厨师做一道菜手艺好就行服务化是整家餐厅的运营既要考虑厨房有多少灶台又要考虑排队怎么排、高峰期怎么应对、客人吃了一半等太久怎么办、后厨出问题怎么处理。把模型跑起来相当于你有了一个手艺不错的厨师但距离一家能盈利、口碑好的餐厅还有很多东西要补。1.2 为什么不能用本地体验推断线上性能很多人犯一个特别常见的错误在本地单次调用模型觉得速度挺快就推断线上也能扛住。这里有个核心误区单请求延迟由模型计算速度决定而服务化容量由吞吐和排队共同决定两者根本不是一回事。本地单请求测试时GPU整卡算力全部服务于一个请求模型当然跑得快。线上不一样假设模型单请求生成速度是30 token/s一个用户会觉得挺好但如果有20个并发用户理想情况下大家共享一张卡每个请求获得的算力就被均分了。更麻烦的是如果框架调度策略不好一个长上下文请求会长时间霸占GPU其他短请求全部在后面排队。我自己的经验是本地测出来的单请求延迟至少要打个三折到五折才接近线上P50的水平。P99就更难说了因为P99对排队时间、系统抖动、长尾请求都极其敏感。服务化设计的核心目标不是“让单个请求更快”而是在负载下让系统吞吐最大化同时守住延迟的SLA。2. 中间隔着的那几块硬骨头2.1 显存危机KV Cache比模型权重更能吃显存自回归模型推理有一个很容易被忽略的隐形成本叫KV Cache。模型每生成一个token都需要把所有历史token的Key和Value向量缓存起来供后续注意力计算使用。这个缓存大小跟并发数和上下文长度是线性关系而且增长速度特别快。我算一笔账给你看。以7B模型为例假设32层、KV头数8、每个头维度128半精度存储每个token的KV Cache大约占128KB左右。如果最大上下文长度设成8192同时有32个请求在跑那么KV Cache的总占用就是32乘以8192乘以128KB。算下来大概32GB光缓存就吃掉一张主流显卡的全部显存。模型权重还要占14GB左右单卡基本是撑不住的。所以推理框架必须在“模型权重显存”和“KV Cache显存”之间做平衡。vLLM的核心创新PagedAttention就是把KV Cache切成固定大小的页按需分配、用完归还显存利用率大幅提升。传统静态批处理框架经常会一次性把所有请求的KV Cache全部预留空间浪费和碎片问题都很严重。这也是为什么同样是7B模型一个框架可以跑32并发另一个只能跑8并发。给一个非常具体的实操建议无论用哪个推理框架先算清楚自己业务的上下文长度和并发数上界再来决定max-model-len和gpu-memory-utilization参数。我习惯先把模型权重占用的显存留出来剩下的尽量都给KV Cache但缓存利用率不要压到95%以上否则显存碎片一出现容易OOM。2.2 并发与批处理GPU空转的根源GPU的本质是一个吞吐型计算设备设计目标就是大量计算并行执行。理想的推理负载应该是有很多请求同时灌进来把算力塞满。但真实业务里的请求是稀疏的、不均衡的、长短参差的。如果不做任何调度GPU会在大部分时间里空转等请求这在服务化场景下就是纯纯的资源浪费。批处理就是解决这个问题的但要分两种来看。静态批处理的思路是收集一批请求凑成一个batch一次性跑完整个前向过程再一起返回。它的最大问题是batch里所有请求要被最慢的那个拖住第一个完成请求的算力也释放不了。连续批处理continuous batching是更主流的做法把调度粒度从“请求级别”降到“token步级别”。每生成一个token之后系统可以把新到达的请求随机插入到当前batch里同时把已经生成完的请求踢出去。用吃饭来类比静态批处理是一张只接待宴席的餐桌必须等所有人吃完才翻台连续批处理是快餐店一个人吃完就走空位随时让新人坐下。这个机制对吞吐的影响极其明显实测在同样并发下连续批处理的系统吞吐通常比静态批处理高出一倍以上。这也是“离服务化还很远”这句话在技术层面的真实体现。很多人在本地跑模型时根本碰不到批处理问题因为永远是单请求串行调用。但服务化第一天压测吞吐上不去第一反应是“模型太慢”实际上往往是批处理策略没有生效。2.3 延迟与吞吐先定SLA再做取舍服务化性能不能只看单一指标。我建议至少建立三个维度首Token延迟TTFT、Token间生成间隔ITL或TPOT、整体吞吐。这三者的优化方向经常是互相冲突的。想提高吞吐自然会调大batch甚至开启连续批处理。但batch一旦变大单个请求分到的算力就少了token生成速度会下降。当生成速度低于用户的感知阈值比如打字机特效半天出不了一个字体验就会很差。反过来你想讨好单个用户给每个请求独享GPU延迟会很好看但整卡吞吐低得可怜线上只要来十几个并发就满载了其他人开始无限排队。做取舍的正确姿势是先定SLA。比如“P95首Token延迟小于1.5秒”“单请求生成速度不低于15 token/s”。然后压测时不断调整batch上限和并发上限找到一个SLA和吞吐都能满足的甜蜜区间。这个区间不会自己出现必须靠实测标定。2.4 推理框架选型对场景下菜碟选推理框架不能跟风要按业务画像来。我按自己的实战经验把几个主流方案做了一个分类供大家参考。vLLM是大模型服务化的首选项目之一。它的优势在于PagedAttention和连续批处理带来的高吞吐自带OpenAI兼容接口接入成本极低。如果你的需求是快速把一个大模型跑成标准HTTP服务vLLM基本是首选。缺点是部分小众模型架构兼容性要实测长请求调度策略需要自己调参。TensorRT-LLM适合对延迟极度敏感的场景。它会把网络编译成高度优化的TensorRT引擎算子融合和内存复用做得非常狠性能上限通常高于通用框架。但编译流程复杂动态shape支持较弱迭代周期长不太适合快速试错。Triton Inference Server则适合多模型统一管理的生产环境。它本身不针对某个模型做强优化而是提供模型服务化的完整框架可以同时管理多个模型、多个版本、不同推理后端内置动态批处理和模型并发。团队里模型种类多有TensorRT引擎、有PyTorch模型、还有Python推理逻辑时用Triton做统一出口非常合理。Ollama是本地开发的效率工具模型下载、量化、启动都做得很顺手。但它的目标场景是“把模型用起来”不是“把模型服务化扛流量”。如果只是内网自己玩完全够用。真正上生产会遇到并发控制粒度、监控可观测性、弹性扩缩容方面的限制。还有一个值得留意的新方向是GPU资源池化工具比如GPUStack这类方案。它可以把多台机器的GPU统一管理起来在Windows上也能部署解决单机显存不够、多卡管理麻烦的问题。这类工具的价值在资源调度层不是推理引擎本身实战中通常与上面的推理框架配合使用。选型的原则很简单能用最简单的方案满足SLA就不要上更重的架构。我见过不少项目模型才7B直接上了全套微服务加多卡并行最后运维复杂度比业务收益还大。3. 从“脚本能跑”到“能扛流量”的实操记录3.1 第一步建一个可复现的性能基线不管用什么框架第一步永远是先建立一个可重复的执行基线。没有基线后面所有优化都像是在黑夜里猜方向。我自己的标准动作是三组测试。第一组是单请求延迟测试记录TTFT和生成速度。比如用vLLM加载Qwen2.5-7B-Instruct模型输入512个token要求输出128个token记下“首token 0.4秒生成速度约30 token/s”。这组数据就是后面所有优化的参照点。第二组是纯吞吐测试不看单请求快慢只往服务里持续灌请求看整卡每秒能产出多少token。我会依次记录batch1、4、8、16时的吞吐曲线。如果batch到了8以后吞吐不再明显增长说明算力或显存带宽已经见顶再调大batch只会拉长时间没有实际收益。第三组是并发压测这也是服务化真正的试金石。压测工具用locust可以自己写一个协程脚本也没问题关键是统计数据。核心是看不同并发数下的P50、P95、P99延迟、错误率和排队时长。这一轮测出来的数据基本就决定了你的服务能上线、能扛多少流量。3.2 第二步用vLLM把模型跑成标准服务我们用vLLM把模型跑成一个OpenAI兼容服务假设显存是24GB的RTX 4090或相近规格。模型可以先通过业界常见的模型下载方式获取下载慢是国内团队经常遇到的老问题核心原因是源站带宽和出口链路。我的经验是提前把模型配置到指定缓存目录不要默认塞进系统盘不然后面迁移非常痛苦。启动命令大概长这样python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --gpu-memory-utilization 0.90 \ --max-model-len 8192 \ --max-num-seqs 64 \ --enforce-eager \ --served-model-name qwen2.5-7b逐个解释参数。gpu-memory-utilization是0.90意思是允许框架使用90%的显存留一点给其他系统开销。max-model-len是8192表示单请求上下文最长8192个token这个值越大KV Cache占用越高能并发处理的请求数就越少。max-num-seqs是64表示最多同时处理64个序列超过的请求进入队列排队。enforce-eager是关闭CUDA Graph优化适合调试期快启动生产环境我会把这行去掉让CUDA Graph优化编译生效延迟表现会更好但显存占用会相应上升。启动完成以后vLLM会提供一个OpenAI兼容的接口。宿主机器上任何支持OpenAI SDK的客户端都能直接接入。很多人喜欢用Langflow这类低代码编排工具搭建AI应用此时在工具里配置自定义模型服务地址把服务地址、模型名和API Key填进去模型就变成了一个可视化工作流里可直接拖拽使用的节点。服务端稳不稳定直接决定前端编排工具顺不顺手这个体验我深有体会。3.3 第三步压测与容量评估的完整过程服务跑起来之后先不要急着接业务。我看到太多人API一通就上线结果第一次真实流量进来全是超时。正确的顺序是先做一轮压测拿到曲线再定容量。操作方法是把并发数从1开始逐级往上加例如1、4、16、32、64。每级并发都记录三个关键数字QPS、P99延迟、错误或超时率。大概率你会得到这样一条曲线刚开始并发翻倍QPS接近线性增长延迟还在可接受范围到某个临界点之后QPS涨不动了延迟开始指数级爬升错误率抬头。这个临界点就是这台机器、这个配置下的实践容量上限。上线时我习惯把实际运行的并发上限压在临界点的60%到70%。这个缓冲空间非常重要当流量突发时系统还有余量等待调度器扩容而不是直接雪崩。很多项目上线翻车就是因为扩容后负载均衡还没来得及生效新节点刚起来又被过量请求打挂陷入恶性循环。容量是测出来的不是估算出来的。拿到自己的压测数据以后再决定配置几个副本。副本管理通常用K8s的HPA或云平台的弹性伸缩来实现。这里有个细节要注意扩缩容的触发指标要选GPU利用率而不是CPU利用率。模型推理负载下CPU通常很闲GPU才是瓶颈选错指标会导致永远不触发扩容。3.4 第四步补齐限流、超时、监控、灰度四块拼图推理框架解决了高性能计算问题但它本身不是完整的服务化产品。真正能扛住生产流量的模型服务还要补齐四块拼图。第一块是限流和准入控制。当流量超过服务容量时最好的策略不是让所有请求无限排队而是快速拒绝或降级。比如配置最大排队长度超出以后直接返回明确的“模型繁忙”错误。很多人讨厌这个错误但比起让请求无限排队直到超时快速失败体验反而更友好。用户知道要重试或者走降级链路。不过这里有个配套要求客户端必须做指数退避重试否则所有被拒请求同时重试会产生晚高峰级的重试风暴。第二块是超时与熔断。每个请求必须有明确超时时间框架层的调度超时和业务层的生成超时要分开设置。如果请求生成到一半超时了系统要能及时释放它占用的KV Cache和batch槽位而不是让它继续占着算力执行完。熔断则是上游调用方的自我保护当模型服务连续超时或出错调用方要能立刻转入降级逻辑而不是死等模型恢复。第三块是监控与日志。至少要看这么几类指标GPU利用率和显存占用、QPS和请求量、延迟分布TTFT、TPOT、P95/P99、排队长度、错误率和超时率。日志要能串联到一次请求从进入到返回的全过程。模型服务一旦开放接口就必须能回答一个问题这个慢请求是从哪里来的卡在哪一步。第四块是版本管理和灰度发布。模型服务上线以后模型本身也会持续迭代今天微调一版明天换一种量化方式。生产环境必须支持多版本并存、灰度切换和快速回滚。Triton这类框架原生支持模型版本目录vLLM可以部署两个服务实例再切流量来模拟灰度。本质都是先把新版本验证通过再把流量逐步切过去。4. 常见问题与排查技巧实录4.1 “模型繁忙”的真实原因与处置策略“模型繁忙”是你上线后最常遇到的报错但我排查过的案例里一大半都不是框架卡死而是容量设计出了问题。拿到错误后的第一件事是看日志里的队列长度和请求到达率。如果请求到达率持续超过处理率队列不断堆积说明容量不足需要加副本。如果队列并不长但延迟很高说明batch配置过大单个请求被同batch里的其他请求拖住了。另外一个很容易被忽视的原因是客户端重试策略。无脑重试会让服务端在雪崩边缘反复横跳。正确做法是客户端采用指数退避同时在服务端为不同来源的流量划分优先级内部批处理任务和用户实时请求要分成不同队列不能让它们互相拖累。4.2 显存OOM先看日志再动参数模型服务上线之后显存OOM很多人第一反应是把max-model-len调小或者降低GPU显存利用率。这两个动作确实有用但属于治标不治本。更关键的是要先判断OOM发生在哪个阶段。启动阶段就OOM多半是框架一次性预分配太大比如同时设置了很大的max-model-len和很大的max-num-seqsKV Cache从启动那一刻就把显存打满了。运行一段时间以后才OOM通常是因为显存碎片或者出现了超出预期的超长上下文请求。碎片问题可以通过调节gpu-memory-utilization来缓解给系统留出碎片整理的余地。服务器显存不要全部打满打满了就是给自己留坑。稳定运行比榨干最后一兆显存重要得多。4.3 并发一上去P99延迟直接开飞机这个现象在并发上升以后非常常见。通常有三个原因一是混入了超长上下文的请求单步计算量巨大二是连续批处理把短请求和长请求塞在一起长请求拖慢整批进度三是GC停顿或日志量大等系统级抖动。排查顺序建议先看排队时间再看生成吞吐。如果排队时间不高但token生成速度明显下降基本可以锁定是批内长短请求互扰。解决方案是把长上下文请求和短请求拆到不同的处理队列或者为不同档位的上下文长度配置不同的max-model-len上限让调度器分开处理。这个办法我在很多项目里都用过见效非常直接。4.4 模型下载、存储路径、部署环境这些日常坑下载模型慢、磁盘不够、缓存路径不对看起来都是小事一旦发生就会浪费大量时间。我总结了三件固定动作。第一显式配置模型缓存目录别让默认路径挤爆系统盘。第二把常用模型提前同步到内网或本地仓库不提倡每次部署都现场从公网拉取。第三Windows平台部署时注意路径分隔符和CUDA版本兼容如果使用了GPU资源池化工具部署多卡集群把环境变量和用户权限一并梳理清楚能避免很多莫名其妙的权限报错。还有一点提醒本地向量模型、Embedding模型这类看起来很小的模型很多人觉得简单。但它们一旦服务化遇到的显存、并发、批处理问题和大模型完全一样。模型体积小只是权重小不代表工程复杂度低。4.5 别忽略输入侧的安全与质量校验模型服务对外暴露之后就会收到各种来源、各种格式的请求。不管模型本身多强输入端都强制做三层校验一是限制单次请求的最大输入长度和最大生成长度防止超大payload直接把显存打穿二是对输入内容做基础合规过滤让模型服务不能成为违规内容的出口三是对高频异常调用做好监控防止接口被脚本刷向异常状态。这一块不需要引入多复杂的组件但必须作为上线清单的一部分。服务化之后模型就不再是实验品而是面向真实用户的入口它的输入输出都会对真实业务产生直接影响。输入侧的治理是基本功不是加分项。写在最后一个值得记住的真实案例写到这里想起年初的一次线上事故。那晚我盯着监控数据百思不得其解压测明明全部通过流量也是缓慢上涨的系统怎么就扛不住了后来仔细排查发现是扩缩容策略选错了指标——用的CPU利用率做触发条件。模型推理服务的真实瓶颈在GPUCPU一直很空闲于是系统压根没触发扩容流量全靠一台机器硬扛。这种低级错误教科书不会写只有半夜被电话叫醒的人才会记得刻骨铭心。所以最后送大家一句我从实践里总结的话在一个模型服务没有经历过真实流量压测之前永远不要相信它的稳定性。先把压测、监控、限流三件事做扎实后面所有模型迭代都只是换一个权重文件而已。这也是“推理系统”和“服务化”之间真正需要跨越的距离。
阅读完成 · 觉得有帮助?