大模型推理服务全流程拆解:从显存规划到性能优化实战 1. 推理服务在整个大模型落地链条里到底管哪一段先抛一个特别常见的现象很多人花了两三个月把模型选型、微调、数据清洗跑通模型权重也拿到手了结果卡在了“怎么把它变成一个能稳定跑线上请求的接口”。网上关于大模型训练、微调的资料铺天盖地但“大模型推理服务工作流程”讲清楚的并不多。我自己从最早在单卡上折腾脚本到后来维护一个每天上万次请求的推理服务中间踩过的坑足够写满一页笔记。这篇文章就按我实际搭过、改过、救火过的路线把一套完整的推理服务工作流程掰开揉碎讲一遍。很多人会把推理服务简单理解成“把模型 weight 加载到显存里然后跑一个 Python 脚本接收输入、调用 model.generate() 返回结果”。如果是写 demo这么做没问题一旦要对外提供服务立刻会撞上几个问题并发请求来了怎么排队第一个用户还没生成完第二个用户会不会把显存撑爆为什么同一个模型在不同服务框架下吞吐能差好几倍为什么批量上去之后响应时间反而变慢1.1 从模型权重到可用服务中间缺的是一套“基础设施”一个训练好的模型权重本质上只是一堆张量参数它并不天生具备“服务”的形态。你要让它对外可用至少需要解决这么几件事请求怎么进来HTTP 接口、鉴权、限流、超时处理。模型怎么推理不能一次只处理一个请求要支持并发把 GPU 的算力和显存用起来。中间结果怎么缓存多轮对话、相同前缀的系统提示词如果每次都重新算一遍浪费惊人。服务怎么扩容与恢复进程挂了怎么拉起、卡住了怎么自动重启。怎么观测延迟、吞吐、显存、队列积压、失败率这些东西没有监控就是在裸奔。一台裸服务器上加一个模型权重永远不等于一个推理服务。把这层补上就是“推理工作流”这个问题的核心。1.2 推理服务和训练服务逻辑完全不同训练和微调关注的是 batch 尽可能大、数据吞吐尽可能高哪怕单个 step 慢一点没关系最终目标是让 loss 降下去。推理服务不一样它在意的是一次请求从发起到返回的可感知延迟以及在多个请求同时到达时整体的吞吐。而且推理的生成是自回归的——模型每次只能生成一个 token这一个 token 需要作为输入的一部分重新进入模型生成下一个 token。你问它一句十个字的话它可能在内部循环了几百次才把回答吐完。我经常跟团队里新同学打比方训练是在工厂里批量生产一批零件慢一点没关系要的是整条产线每分钟的产出推理是开了一个二十四小时营业的饭馆客人坐下点了菜后厨既要保证每桌不饿死又要在高峰期尽可能多翻台。推理服务做的就是饭馆前厅加后厨的管理模型权重只是那本菜谱。2. 一条提示词从进入到返回中间到底经历了什么我自己刚接触推理服务时最容易搞混的就是整个链路里每个组件到底在干什么。其实拆开来看并不复杂一条用户请求从 client 发出到拿到完整回答大致经过下面这些环节客户端发起请求通常是一个 HTTP POSTbody 里带 prompt、参数、上下文。网关层做路由、鉴权、限流。推理服务的 API 层解析请求做参数校验、文本转 token。调度器判断现在 GPU 上有没有空位让哪些请求排队哪些请求插队。推理引擎执行 prefill预填充把整段输入一次性算完和 decode逐 token 生成的循环。生成结束后token 转回文本通过 HTTP 流式响应一点点返回给用户。2.1 请求入口层为什么大家都在做“OpenAI 兼容”格式现在开源的推理服务框架默认都会提供一个 OpenAI 风格的 HTTP 接口比如/v1/chat/completions和/v1/completions。这个格式已经成了事实标准。不管底层跑的是 vLLM、Ollama 还是 TensorRT-LLM只要暴露的接口协议一致上层应用就可以不做任何修改自由切换后端。这一步的重要程度经常被低估。如果业务代码和推理服务强耦合今天接的是 A 框架明天想换 B 框架所有调用逻辑都要改一遍上线节奏就被拖死。统一成 OpenAI 兼容格式之后换后端只是换个 base_url 的事。我们当时在公司内部做统一推理平台第一条规则就是所有模型服务必须提供兼容格式的 HTTP 接口其他能力可以各自发挥但这条不能妥协。2.2 Prefill 阶段为什么输入越长首个 token 出来越慢请求进入引擎后第一件事是 prefill。输入提示词是一整段文本模型会把这段文本的全部 token 一次性并行计算生成每一层的 Key 和 Value 向量存到 KV Cache 里这一步也是计算密集的。注意prefill 的耗时和你输入的 prompt 长度是强相关的。prompt 有 100 个 token模型要并行算 100 个位置prompt 有 2000 个 token算的量直接涨一个量级。这也是为什么同样一个模型在冷启动时第一个 token 返回时间TTFTTime To First Token会随着上下文变长而明显变大。我遇到过不少业务方问“为什么我的 input 就几千字接口却要等好几秒才出第一个字”这就是 prefill 在起作用。它不是在做网络传输是在为整段输入建索引后面每个新 token 的生成都得基于这一层计算结果。2.3 Decode 阶段一个 token 一个 token “挤”出来prefill 结束之后进入 decode 循环。模型根据当前的 KV Cache 和最后生成的 token预测下一个最可能的 token然后把新 token 追加到序列末尾更新 KV Cache再预测下一个。这个循环会一直执行到满足停止条件通常是生成结束、达到 max_tokens或者是模型生成了结束符。decode 阶段每个 step 只生成一个 token计算量不大但必须串行执行。吞吐优化的难点就在于如何让多个请求的 decode 阶段挤在一起并行计算。这个阶段的关键指标是 TPOTTime Per Output Token也就是平均每个输出 token 要多少毫秒。用户体验上输出越快打字机一样的流式效果就越流畅。理想情况下decode 阶段应该做到每 token 50 毫秒以内也就是每秒 20 token 以上这样人眼感觉基本是连贯的。3. 推理引擎选型vLLM、Ollama、TGI、llama.cpp 到底该用谁到了真正部署的时候马上会遇到一个绕不开的问题用什么引擎跑模型。下面对比的是我实际用过且目前社区里最常见的几个方案。可能有人会问Pytorch 里加载模型直接 generate 不就行了吗可以但如果你追求并发吞吐和显存效率原生推理脚本几乎没有优化请求一多就崩所以需要专门的推理框架/引擎。3.1 生产环境并发服务我首选 vLLMvLLM 是目前生产环境里我最常用的推理框架。它最大的卖点是引入了 PagedAttention 技术把 KV Cache 切分成固定大小的块像操作系统管理内存一样管理显存从而大大减少显存碎片支持更多并发请求。除此之外vLLM 的 Continuous Batching连续批处理让调度器不再等一个请求完全结束才释放资源而是每生成一个 token 之后都重新检查有哪些请求可以插进来资源的利用率会比传统静态批处理高很多。生产上我用 vLLM 起一个 Qwen 系列模型的 OpenAI 兼容服务基本命令类似这样python -m vllm.entrypoints.openai.api_server \ --model /opt/models/Qwen2.5-7B-Instruct \ --tensor-parallel-size 2 \ --max-model-len 32768 \ --gpu-memory-utilization 0.9 \ --host 0.0.0.0 \ --port 8000这个命令里几个参数很关键--tensor-parallel-size 2表示用两张卡张量并行--max-model-len 32768直接决定了上下文窗口上限也直接影响了 KV cache 预留策略--gpu-memory-utilization 0.9是允许引擎最多占用 90% 的显存剩下的留给 CUDA context 和其他进程。配置环境很多细节问题都出在这几个参数上后面专门说。3.2 Ollama 和 llama.cpp 更适合什么样的场景Ollama 应该是近一年个人电脑上部署大模型最方便的工具。它把模型下载、量化、运行、API 服务全部封装成了几条简单命令。在 MacBook 上装完 Ollama 之后ollama run qwen2.5:7b就能把一个小尺寸模型跑起来。对个人学习、前端联调 demo、写小工具来说这是体验最顺的一条路径。llama.cpp 以及它的各种绑定则更适合在自己的硬件上跑 GGUF 格式的量化模型资源占用低、CPU 也能跑。它支持 4-bit、5-bit、8-bit 的模型量化7B 模型量化到 Q4 之后可能只需要 5GB 左右内存。但它更像一个底层跑模型的最小引擎要自己做接口封装和并发控制。TGI 是 Hugging Face 出品的推理服务它的定位和 vLLM 类似但优化思路略有差异。TensorRT-LLM 是 NVIDIA 的闭源优化方案在特定 GPU 上把算子优化做到极致适合那些单卡或整机型号固定、追求极限吞吐的团队代价是编译和适配成本较高模型版本迭代时维护成本也不小。我给你的选择建议是个人调试选 Ollama自研系统接口选 vLLM如果团队对延迟要求极度苛刻且 GPU 型号统一再考虑 TensorRT-LLM。3.3 本地部署与 Docker 化要注意的资源分配问题很多同学从环境搭建开始就用 Docker 部署大模型。容器本身没有太多特殊之处但有几个非常容易出问题的地方我提醒一下显存是 GPU 的资源不是 Docker 的内存参数能限制的必须通过--gpus参数和 NVIDIA 容器工具包显式指定。Docker 默认的共享内存是 64MB某些推理框架的多进程/DataLoader 会用到/dev/shm很可能莫名其妙报错。建议启动时加上--shm-size16g。如果同一台机器上跑多个模型服务必须用CUDA_VISIBLE_DEVICES或 docker 的--gpus device0把不同模型隔离到不同卡上否则 CUDA 默认会看见所有卡可能把模型分散到多张卡上导致性能异常。4. 显存、并发与吞吐推理服务的资源规划方法推理服务的资源规划比训练还容易让人头大因为“模型能不能跑起来”和“跑得好不好”中间隔着一整片显存策略。很多场景不是模型太大放不下而是 KV Cache 没规划好导致并发一上来就 OOM。Kubernetes 这种容器平台不会自动帮你处理 GPU 显存的调度粒度真出问题的时候还得靠基础参数规划。4.1 KV Cache 体积估算决定你能撑多少并发先给一个实用公式用于估算某个模型在给定上下文长度下每个请求占用多少 KV CacheKV Cache 大小 2 × 层数 × KV 头数 × 头维度 × 精度字节数 × 序列长度这个公式的 2 代表 Key 和 Value 两份缓存。以 Qwen2.5-7B-Instruct 为例层数 28、KV 头数 4、头维度 128、模型权重按 FP16 存储那么每个 token 的 KV Cache 是2 × 28 × 4 × 128 × 2 57344 字节 ≈ 56KB当一个请求上下文长度是 4096 token 时这个请求要占 4096 × 56KB 229MB 左右。如果并发 8 个请求都跑到 4096 token光 KV Cache 就要接近 2GB。加上模型权重本身约 14GBFP16一张 24GB 的卡在这个配置下其实已经比较吃紧了。有了这个估算方法你就能明白为什么max-model-len设得越大能同时处理的请求数量就越少。实际做容量规划时我会按“目标并发数 × 平均上下文长度 × 单 token KV 大小”来预留。注意这只是粗略估算实际引擎的预分配和显存碎片还会带来额外开销。4.2 Continuous Batching 是如何提高吞吐的没有 Continuous Batching 的 naive 模式一个 batch 里的请求必须等所有请求都生成完才能让下一批请求进来。实际场景中每个序列的长短差异极大有的请求只输出 10 个 token有的要输出 1000 个 token大家一起被一个慢请求拖住GPU 白白空转。Continuous Batching 的做法是在每个 decode step 结束之后都根据当前每个请求的状态重新组织 batch。已经生成完的序列退出等待中的请求可以随时加入。这样每一轮 GPU 计算时矩阵乘法的“长方形”里几乎都在做有用的事整体吞吐能提升好几倍。这也是为什么在生产环境起多路备份服务时光看 GPU 利用率并不够。可能出现利用率拉满但实际吞吐一般的情况因为请求长短不齐、某些 step 里显存里堆了太多已结束但没来得及清理的旧序列。引擎层调度逻辑才是吞吐瓶颈的关键。4.3 三个需要反复调的关键参数max-model-len、max-num-seqs、gpu-memory-utilization这三个参数是在 vLLM 这类引擎里最影响容量和并发瓶颈的max-model-len模型支持的最大序列长度。不要盲目设到几十万除非确实需要。设得越长模型权重之外留给 KVCache 的可用空间在同样的显存下越紧张支持的并发越少。max-num-seqs同一时刻允许在 GPU 上并行推理的最大序列数。设太大可能导致显存不足设太小吞吐上不去。建议从 8 到 16 开始试观察显存余量后逐步往上加。gpu-memory-utilization让引擎最多使用多少比例的显存做推理。0.9 是我常用的值因为要留一部分显存给 CUDA context、驱动和可能的临时 tensor。如果卡同时跑着别的任务这个值要更低。有一句话我得反复强调参数之间是互相制约的。不要单独把一个参数调大到极限然后抱怨并发上不去。显存是一块有限的饼模型、KV Cache、运行开销都在分这一块饼。5. 上线后最常遇到的坑OOM、输出中断、缓存命中率低再好的框架也不可能完全避开运营期的意外下面挑几个我实际处理过且频率最高的问题把排查链路完整写出来。5.1 服务运行一段时间后随机超时或报错先不要动代码曾经有个内部服务上线一周后每天下午开始出现请求超时重启之后又能撑一阵。一开始团队怀疑是代码问题把日志翻了半天最后发现是 GPU 显存随着每天 KV Cache 的积累越来越满最终调度器把新请求拒绝或排队时间变长导致从外部看就是超时。排查这类问题最快的方法是看这三样东西nvidia-smi观察显存占用是否持续增长直到打满。引擎日志里有没有 “OutOfMemory” 或 “KV cache is full” 的字样。请求排队队列长度是否在某个时间点开始不断累积。如果是 KV Cache 导致的问题不要只想着重启。适当调低gpu-memory-utilization或max-num-seqs给显存留出缓冲比隔几天重启一次健康得多。5.2 流式输出中途断开不一定是模型的问题现在很多场景用流式接口让用户看到打字机效果。有些同学会发现输出到了某个位置突然断开界面上只显示了一半回答。最开始我以为是后端代码 bug后来抓包发现是反向代理或网关层的请求超时时间太短大模型生成 1000 个 token按每秒 20 token 算需要 50 秒如果网关设置的读超时是 30 秒连接就会被切断客户端自然收不到后半段。另一个常见原因是前端在解析流式响应时错误地把某些分片数据当成完整 JSON 解析一旦中间有个包被截断就认为流结束了。所以排查“输出中断”时先看是客户端状态码断连、网关断连还是引擎侧真的生成了 EOS 结束符不要一上来就改模型温度。5.3 缓存命中率怎么优化才能不重复计算一样的 prompt搜索引擎里“vllm 如何优化缓存命中率”成为热词说明很多人已经关心这个问题。vLLM 支持 prefix caching / automatic prefix caching。如果两条请求的前缀 token 完全一致后面的请求可以直接复用之前算好的 KV Cache跳过 prefill从而明显降低首个 token 延迟。想提高命中率可以从这几个方向入手系统提示词固定不变永远放在用户消息之前不要随机插入时间戳、无意义的换行。固定一轮对话的格式例如历史对话始终按同样的角色标记和拼接规则组装。避免在相同语义的位置插入动态变化的 token哪怕是一个空格也会导致前缀不一致缓存就无法命中。这里有一个反直觉的点前缀完全相同才命中不是“语义相近”。有些业务会把用户 ID 拼在 prompt 最前面导致每个用户的前缀都不一样缓存命中率几乎为零。正确的做法是把用户无关的固定说明和 few-shot 示例放在最前面把变化的部分往后放尽可能让不同请求共享最长公共前缀。6. 从个人电脑到生产集群不同场景下的工作流差异大模型推理服务的“工作流”不是一个固定答案。普通开发者在 MacBook 上本地部署大模型和生产环境用多卡集群跑高并发服务是完全不同的两套玩法需要面对的问题也不一样。6.1 本地部署MacBook Air M3 16G 这类设备能做什么很多人在搜“macbook air m3 16g 要部署大模型进行开发应该用哪个工具”。我直接说结论Mac 的统一内存架构跑中小尺寸模型确实可行但必须用量化模型和合适的推理工具。比如在 Ollama 里跑 qwen2.5:7b 的 Q4 量化版本模型占用大概 5GB 左右如果只是偶尔跑跑测试、上下文不长16G 统一内存的机器是可以胜任的只是不要指望它能支撑大并发。个人电脑上建议把推理服务当作“本地开发调试依赖”而不是“生产服务”。例如接入 VS Code Continue 这类插件开发时通过 Ollama 暴露本地接口然后让编码助手插件走本地模型调试链路短、不需要把代码和 prompt 传到外部对隐私敏感的开发场景也有价值。至于“用哪个工具”和“部署哪个模型”的结论我的经验是Mac 上用 Ollama优先选 Qwen 这类对中文支持好的开源模型显存不够就选 7B/8B 的 Q4 量化版。6.2 多卡与多机场景模型并行不只有一种切法当单卡放不下模型或者单卡性能不够就要上多卡推理。最常用的是张量并行把一层里的矩阵按维度切成多份分到不同卡上计算每一层算完做一次 allreduce 同步。这种方案通信频率高需要卡间有高带宽最好在同一台机器里通过 NVLink 互联。vLLM 开启方式就是设置--tensor-parallel-size 4表示 4 卡并行。如果单机多卡放不下就需要流水线并行把不同层放在不同卡上串行执行。这种方案对卡间通信带宽的要求稍低但容易出现某一级卡在等待效率不如张量并行。生产环境常见做法是先用张量并行把一张“逻辑大卡”拼出来不够再叠加其他方式但复杂度会明显上升。不要一上来就搞多级并行先用一份小流量实验验证收益再逐步放量。6.3 推理服务最该监控的几个数字说句得罪人的话很多团队把服务部署起来之后监控面板上只有 CPU 和内存等于没有监控。做推理服务至少要有这几个指标GPU 显存占用率和利用率判断当前负载和瓶颈。TTFT首 token 延迟反映 prefill 和排队压力。TPOT 或输出速度token/s反映 decode 阶段是否流畅。请求排队长度排队越长用户体验越差。成功请求数、失败数、超时数最基本的健康信号。每次容量调整、并发参数修改之后都要回到这些指标上看效果而不是只凭感觉“快了还是慢了”。推理服务的优化本质上是显存、延迟、吞吐三者的平衡没有一套参数能通吃所有场景。我自己动手搭了几年推理服务最大的体会是不要被框架名目搞晕先想清楚你的场景到底是“自己写代码调模型”还是“给外部提供稳定 API”场景定了引擎和流程会自己浮出水面。哪怕前面这些参数一时没设好也不用慌照着监控指标一点点调大部分性能问题都能在数据和日志里找到答案。