
1. 从单模型到推理平台部署这件事到底在解决什么问题模型部署这个词听起来像是运维的活儿但真正做过的人都知道它横跨了算法、工程、硬件、网络四个领域。你训练出一个模型准确率再高如果推理延迟 3 秒、吞吐量每秒 2 个请求、显存溢出崩一次那它在生产环境里就是不可用的。我见过太多团队在实验室里跑通了 demo一上线就发现 QPS 撑不住、GPU 利用率不到 30%、批处理逻辑写错了导致结果串号。所以部署框架的选择本质上是在回答一个问题你的模型以什么形态、什么成本、什么稳定性对外提供服务。从最早的 Flask 包一个 PyTorch 模型到后来的 TorchServe、Triton Inference Server再到如今 vLLM、SGLang 这类专门为大语言模型设计的推理引擎整个技术栈的演进路线非常清晰。单模型服务解决的是“能跑起来”的问题推理平台解决的是“跑得稳、跑得省、跑得快”的问题。这两者之间的差距不是简单加几台机器就能填平的它涉及到连续批处理continuous batching、PagedAttention 显存管理、KV Cache 复用、多模型编排、动态扩缩容等一系列工程难题。这篇文章适合谁看如果你手头有一个训练好的模型想把它变成 API 服务那前面的单模型部署部分对你有用。如果你已经在维护多个模型、多个版本正在被 GPU 利用率和成本问题困扰那后面关于推理平台和 vLLM 的内容会更贴合你的需求。如果你只是好奇 LLM 推理平台到底在做什么我也会用生活化的类比把核心原理讲清楚。整篇内容基于我在实际项目中的选型经验和踩坑记录不堆砌官方文档的复述重点讲清楚“为什么这么选”和“怎么落地”。2. 单模型服务从 Flask 到 Triton 的选型逻辑2.1 什么时候用 Flask/FastAPI 直接包模型就够了很多人一上来就问“部署框架选哪个”但这个问题缺少一个前提你的模型有多大、请求量有多少、延迟要求是什么。如果你的模型是一个 100MB 以内的传统深度学习模型比如 YOLOv5、BERT-base、ResNetQPS 在 10 以下那说实话Flask 或 FastAPI 直接包一层就足够了。我试过用 FastAPI 部署一个 YOLOv5s 模型单卡 T4输入 640x640端到端延迟稳定在 25ms 左右QPS 跑到 8 的时候 GPU 利用率才 40%。这种场景下引入 Triton 或 vLLM 反而是过度设计增加了运维复杂度收益却很小。FastAPI 的优势在于开发速度快、调试方便、生态成熟。你可以用 Pydantic 做请求校验用 Uvicorn 做 ASGI 服务器用 Gunicorn 做进程管理。一个典型的单模型服务代码结构大概是这样的from fastapi import FastAPI from pydantic import BaseModel import torch app FastAPI() model torch.load(model.pt, map_locationcuda) model.eval() class Request(BaseModel): input_data: list app.post(/predict) async def predict(req: Request): with torch.no_grad(): tensor torch.tensor(req.input_data).cuda() output model(tensor) return {result: output.cpu().tolist()}这段代码能跑但有几个坑要注意。第一模型加载要在服务启动时完成不能放在请求处理函数里否则每次请求都重新加载模型延迟直接爆炸。第二要显式调用 model.eval()否则 Dropout 和 BatchNorm 的行为会和训练时不一致导致推理结果不稳定。第三torch.no_grad() 必须加不然 PyTorch 会构建计算图显存占用会翻好几倍。第四如果用的是 GPU要注意请求并发时的显存竞争问题多个请求同时推理可能会导致 OOM。提示FastAPI 默认是单进程的如果你用uvicorn main:app启动它只用一个 CPU 核心。生产环境建议用gunicorn -k uvicorn.workers.UvicornWorker -w 4 main:app启动多个 worker但要注意每个 worker 都会加载一份模型显存占用会成倍增加。2.2 Triton Inference Server 解决了哪些工程痛点当你的模型数量超过 3 个或者你需要同时服务 PyTorch、TensorFlow、ONNX 等多种格式的模型时Flask 那套就力不从心了。Triton 的核心价值在于统一了多框架、多模型、多版本的管理。你不需要为每个模型写一套服务代码只需要按照 Triton 的目录规范放置模型文件它就能自动加载并提供统一的 gRPC/HTTP 接口。Triton 的模型仓库目录结构是这样的model_repository/ ├── yolov5/ │ ├── 1/ │ │ └── model.pt │ └── config.pbtxt ├── bert/ │ ├── 1/ │ │ └── model.onnx │ └── config.pbtxt └── ensemble_model/ ├── 1/ └── config.pbtxt每个模型目录下的数字文件夹代表版本号Triton 支持热更新你只需要新增一个版本目录它就能自动加载新版本而不中断服务。config.pbtxt是模型配置文件里面定义了输入输出的名称、维度、数据类型以及使用的后端框架PyTorch、TensorRT、ONNX Runtime 等。Triton 最实用的几个功能动态批处理dynamic batching可以把多个小请求合并成一个大 batch 一起推理显著提升 GPU 利用率模型集成ensemble可以把预处理、推理、后处理串成一条流水线减少网络往返实例组instance group可以控制每个模型在 GPU 上启动几个推理实例方便做资源隔离。不过 Triton 的学习曲线比较陡config.pbtxt的配置项非常多稍有不慎就会加载失败。我踩过的一个坑是输入输出的维度顺序要和模型实际期望的一致特别是图像模型NCHW 和 NHWC 搞反了不会报错但推理结果会完全错误。另一个坑是动态批处理的最大 batch size 要合理设置设太大容易 OOM设太小起不到批处理的效果。2.3 单模型服务的性能瓶颈在哪里不管你用 Flask 还是 Triton单模型服务最终都会遇到几个瓶颈。第一个是GPU 利用率上不去因为请求是串行到达的GPU 在等待请求的间隙处于空闲状态。第二个是显存碎片化特别是 LLM 场景下KV Cache 的动态分配会导致显存碎片最终无法分配连续的大块显存。第三个是模型切换成本高如果你有多个模型需要轮流使用每次切换都要重新加载权重耗时可能达到几十秒。这些问题在传统深度学习模型上还不算致命因为模型小、推理快、显存占用低。但到了 LLM 场景7B 参数的模型 FP16 精度下光权重就要占 14GB 显存推理时 KV Cache 还要额外占用大量显存单模型服务的架构就彻底撑不住了。这就是为什么需要 vLLM 这类专门为 LLM 设计的推理引擎。3. LLM 推理引擎vLLM 为什么成为事实标准3.1 PagedAttention 和连续批处理的核心原理vLLM 最核心的两个技术是PagedAttention和连续批处理Continuous Batching。这两个词听起来很学术但用生活类比就很好理解。先说 PagedAttention。传统的注意力机制在推理时每个请求的 KV Cache 需要一块连续的显存空间。这就像你去停车场停车每辆车必须停在一个完整的连续车位上不能跨车位停放。问题是LLM 的输出长度是不确定的你不知道用户会生成 10 个 token 还是 1000 个 token所以你得按最大长度预留显存。这导致大量显存被浪费而且不同请求的 KV Cache 大小不一容易产生碎片。PagedAttention 的思路来自操作系统的虚拟内存分页。它把 KV Cache 切成固定大小的块block每个块可以存放在显存的任意位置通过一个页表来映射逻辑块和物理块的关系。这就像停车场不再要求连续车位而是把车位切成小格子每辆车可以停在不同位置的格子里只要记录好哪些格子属于哪辆车就行。这样一来显存利用率可以从原来的 20%-40% 提升到 90% 以上。连续批处理解决的是另一个问题。传统批处理是静态的凑齐一批请求一起推理等这批全部完成后才能处理下一批。如果这批里有一个请求要生成 1000 个 token其他请求只生成 10 个 token那其他请求早就完成了但 GPU 还在等那个长请求造成浪费。连续批处理的思路是每个推理步骤结束后完成的请求立刻退出新的请求立刻加入。这样 GPU 始终处于满负荷状态吞吐量可以提升 5-10 倍。3.2 vLLM 部署实操从 Docker 到生产环境vLLM 的部署方式有很多种最推荐的是 Docker 方式因为依赖管理最干净。官方提供了vllm/vllm-openai镜像直接集成了 OpenAI 兼容的 API 服务。一个典型的启动命令是这样的docker run --gpus all \ -v /path/to/models:/models \ -p 8000:8000 \ vllm/vllm-openai:latest \ --model /models/Qwen2.5-7B-Instruct \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --dtype auto几个关键参数需要解释一下。--tensor-parallel-size是张量并行数如果你有多张 GPU可以设为 GPU 数量vLLM 会自动把模型切分到多卡上。--max-model-len是最大上下文长度这个值直接影响 KV Cache 的显存占用设太大容易 OOM设太小会导致长文本请求被截断。--gpu-memory-utilization是 GPU 显存利用率上限默认 0.9意思是 vLLM 会尝试占用 90% 的显存留 10% 给系统和其他进程。--dtype auto会让 vLLM 自动选择精度通常是 FP16 或 BF16。如果你用的是消费级显卡比如 RTX 4090 24GB部署 7B 模型是没问题的但 14B 模型就比较勉强了。我实测下来Qwen2.5-7B-Instruct 在 FP16 精度下权重占约 14GBKV Cache 按 8192 上下文算大约占 2-3GB总共 17GB 左右24GB 显存够用。但如果你要部署 14B 模型建议用量化版本比如 GPTQ 或 AWQ 4bit 量化权重可以压缩到 8GB 左右。注意vLLM 的 Docker 镜像对 CUDA 版本有要求vllm/vllm-openai:latest通常需要 CUDA 12.1 以上。如果你的驱动版本较老需要选择对应 CUDA 版本的镜像标签。另外vLLM 在 Windows 上的支持一直不太好官方推荐用 Linux 环境Windows 用户建议用 WSL2 或者 Docker Desktop。3.3 vLLM 的常见坑与调优经验vLLM 用起来简单但调优有不少门道。第一个坑是显存碎片问题。虽然 PagedAttention 已经大幅减少了碎片但如果你的请求长度差异极大比如有的请求 100 token有的 8000 token仍然可能出现显存不足的情况。解决办法是设置--max-num-seqs限制并发请求数或者用--swap-space开启 CPU 交换空间。第二个坑是首 token 延迟。vLLM 的连续批处理优化的是吞吐量但首 token 延迟Time to First Token可能不如预期。如果你对首 token 延迟敏感可以调整--max-num-batched-tokens参数减小批处理的最大 token 数让新请求更快被调度。第三个坑是模型加载时间。7B 模型从磁盘加载到 GPU 大约需要 30-60 秒如果你频繁重启服务这个时间成本很高。建议用--model指向本地 SSD 路径不要用网络存储。另外vLLM 支持--load-format参数可以指定safetensors格式加载比 PyTorch bin 格式快一些。第四个坑是多卡并行的通信开销。如果你用--tensor-parallel-size 2部署两张卡之间需要频繁通信如果卡间没有 NVLink走 PCIe 的话通信延迟会明显增加。实测下来两张 4090 走 PCIe 4.0 x16 做张量并行吞吐量提升只有 1.5 倍左右而不是理想的 2 倍。4. 推理平台全景从 vLLM 到多模型编排4.1 推理平台需要具备哪些核心能力当你从单模型服务走向推理平台需求就完全不一样了。一个生产级的 LLM 推理平台至少需要具备以下能力多模型管理同时服务多个模型支持动态加载和卸载、自动扩缩容根据请求量自动增减推理实例、流量调度负载均衡、灰度发布、A/B 测试、监控告警GPU 利用率、延迟、吞吐量、错误率、权限与配额API Key 管理、速率限制、用量统计。这些能力不是 vLLM 一个引擎能覆盖的vLLM 只解决了“单个模型怎么推理得快”的问题。你需要一个编排层来管理多个 vLLM 实例一个网关层来做流量调度一个监控层来观测运行状态。这就是推理平台和推理引擎的区别。目前市面上有几个开源的推理平台方案可以参考。KServe是 Kubernetes 原生的模型服务框架支持 vLLM、Triton、HuggingFace 等多种后端自带自动扩缩容和流量管理。Ray Serve是 Ray 生态的模型服务组件适合 Python 原生的大规模部署。GPUStack是最近比较火的一个方案主打轻量级和易用性支持在 Windows 和 Linux 上部署对个人开发者比较友好。4.2 多模型编排的实操方案假设你有一个场景需要同时服务一个 7B 的对话模型、一个 0.6B 的 embedding 模型、一个 reranker 模型。这三个模型的资源需求不同对话模型需要 GPUembedding 模型和 reranker 模型可以用 CPU 或者共享 GPU。怎么编排我的做法是用vLLM 服务对话模型用 Triton 服务 embedding 和 reranker 模型前面加一个 FastAPI 网关做路由。网关根据请求路径分发到不同的后端同时做 API Key 校验和速率限制。这样每个模型可以用最适合的引擎资源利用率最高。如果你用 Kubernetes可以用 KServe 的 InferenceService 来定义每个模型的服务KServe 会自动创建 Deployment 和 Service并支持基于请求量的自动扩缩容。一个典型的 KServe 配置是这样的apiVersion: serving.kserve.io/v1beta1 kind: InferenceService metadata: name: qwen-7b spec: predictor: minReplicas: 1 maxReplicas: 4 model: modelFormat: name: vllm runtime: vllm-runtime storageUri: pvc://model-pvc/qwen-7b resources: limits: nvidia.com/gpu: 1这个配置的意思是最少 1 个副本最多 4 个副本根据请求量自动扩缩容。每个副本占用 1 张 GPU。KServe 会监控请求队列长度当队列积压时自动增加副本。4.3 监控与可观测性别等崩了才看日志推理平台的监控比传统 Web 服务复杂得多因为 GPU 是一个黑盒你很难直接看到它内部在干什么。我建议至少监控以下几个指标GPU 利用率用nvidia-smi或 DCGM Exporter 采集、显存占用区分权重占用和 KV Cache 占用、请求延迟P50、P95、P99 分位数、吞吐量tokens/s 或 requests/s、队列长度等待调度的请求数、错误率按错误类型分类。Prometheus Grafana 是标配vLLM 自带 Prometheus 指标导出你只需要在启动时加--metrics-port参数。Triton 也自带指标导出。关键是告警阈值要合理比如 GPU 利用率持续 5 分钟低于 20% 说明资源浪费持续 5 分钟高于 95% 说明需要扩容P99 延迟超过 2 秒说明用户体验受损。提示不要只看平均值。GPU 利用率的平均值 60% 可能意味着有的卡 100% 有的卡 20%负载严重不均。要看每张卡的独立指标以及 P95/P99 分位数。5. 常见问题与排查技巧实录5.1 模型加载失败与显存不足的排查路径模型加载失败是最常见的问题原因通常有几类显存不足、模型格式不兼容、CUDA 版本不匹配、权重文件损坏。排查顺序建议从显存开始因为这是最容易确认的。用nvidia-smi看当前显存占用如果空闲显存小于模型权重大小那肯定是加载不进去的。如果显存够但还是加载失败检查模型格式。vLLM 支持 HuggingFace 格式、GPTQ、AWQ、GGUF 等但不同版本支持的格式不一样。比如 vLLM 0.27.1 对 GGUF 的支持就有限建议用 safetensors 格式。CUDA 版本不匹配通常会在日志里报CUDA error: no kernel image is available for execution on the device这时候需要确认 vLLM 编译时的 CUDA 版本和驱动版本是否匹配。权重文件损坏的情况比较少见但如果你从网络下载模型时中断过可能会遇到。可以用sha256sum校验文件哈希和官方提供的哈希值对比。5.2 推理结果异常与性能骤降的常见原因推理结果异常通常有几个原因精度问题FP16 溢出导致 NaN、tokenizer 不匹配用了错误的 tokenizer 导致输入编码错误、模型版本混淆加载了错误的权重。我遇到过一次部署 Qwen 模型时用了 LLaMA 的 tokenizer结果生成的文本完全是乱码。排查方法是先用一个简单的输入测试比如“你好”看输出是否正常。性能骤降的原因更多。KV Cache 碎片化会导致显存分配失败vLLM 会报OutOfMemoryError或者自动降低并发数。批处理大小设置不当会导致 GPU 利用率低比如--max-num-seqs设得太小请求排队严重。网络带宽瓶颈在多机部署时很常见模型权重通过网络加载会非常慢。CPU 瓶颈在预处理和后处理阶段可能出现特别是图像模型CPU 解码和 resize 可能比 GPU 推理还慢。5.3 常见问题速查表问题现象可能原因排查方法解决方案模型加载 OOM显存不足nvidia-smi查看空闲显存用量化模型或减少max-model-len推理结果乱码tokenizer 不匹配用简单输入测试确认 tokenizer 与模型匹配首 token 延迟高批处理过大查看队列长度和批大小减小max-num-batched-tokens吞吐量低GPU 利用率低监控 GPU 利用率增大并发数或批处理大小服务频繁重启显存泄漏查看日志中的 OOM 记录限制并发数开启 swap多卡并行效率低卡间通信瓶颈检查是否有 NVLink减少张量并行数或换用流水线并行5.4 独家避坑经验分享第一个经验不要在生产环境用 latest 标签的镜像。vLLM 的 latest 镜像更新很频繁有时候新版本会引入 regression。建议锁定具体版本比如vllm/vllm-openai:v0.27.1等新版本稳定后再升级。第二个经验模型文件放在本地 SSD 上。我试过从 NFS 加载模型7B 模型加载了 5 分钟换成本地 SSD 后只要 40 秒。如果模型文件很大可以考虑用内存文件系统tmpfs做缓存。第三个经验压测要在真实流量模式下进行。用ab或wrk压测 HTTP 接口和真实 LLM 请求模式差别很大。LLM 请求的输入输出长度分布很不均匀建议用真实日志回放或者用 Locust 模拟真实用户行为。第四个经验日志要分级。vLLM 的 DEBUG 日志量非常大生产环境建议用 INFO 级别。但排查问题时可以临时开 DEBUG记得排查完改回去否则磁盘很快会被写满。第五个经验GPU 温度要监控。消费级显卡长时间高负载运行温度可能到 80 度以上触发降频。如果发现推理速度突然变慢先看 GPU 温度。机箱风道要做好必要时可以限制功率上限比如nvidia-smi -pl 300把 4090 的功率限制在 300W性能损失不大但温度会低很多。6. 从选型到落地我的部署决策清单6.1 不同场景下的框架选型建议选型没有标准答案但可以根据场景快速缩小范围。个人开发、单模型、低并发FastAPI PyTorch 直接部署简单直接。多模型、需要版本管理Triton Inference Server统一管理多框架模型。LLM 推理、追求高吞吐vLLM 或 SGLangPagedAttention 和连续批处理是刚需。Kubernetes 环境、需要自动扩缩容KServe vLLM云原生方案。Windows 环境、想快速体验GPUStack 或 LM Studio图形化界面友好。如果你的模型是 GGUF 格式llama.cpp 是首选它对 CPU 推理优化很好适合在树莓派或没有 GPU 的机器上部署。如果你需要部署 embedding 模型vLLM 也支持但要注意 embedding 模型的推理模式和生成模型不同需要设置--task embedding参数。6.2 硬件选型与成本估算硬件选型直接决定部署成本。L20 显卡是最近比较热门的选择48GB 显存适合部署 14B-32B 模型功耗 275W性价比不错。RTX 409024GB 显存适合 7B-14B 模型但消费级卡没有 NVLink多卡并行效率低。A100/H100是数据中心卡显存大、带宽高、支持 NVLink但价格昂贵。成本估算要考虑三部分硬件成本、电费、运维成本。一张 4090 大约 1.5 万元功耗 450W按 0.6 元/度电算满载运行一年电费约 2400 元。如果租用云 GPUA100 每小时大约 10-20 元一个月就是 7200-14400 元。所以如果长期使用自购硬件更划算如果只是短期实验租用云 GPU 更灵活。6.3 上线前的检查清单上线前建议逐项确认模型文件是否完整哈希校验、推理精度是否达标和训练时对比、延迟是否满足 SLAP99 延迟、吞吐量是否满足峰值需求压测验证、显存是否有余量留 20% buffer、监控告警是否配置GPU、延迟、错误率、日志是否分级避免磁盘写满、API Key 是否启用防止滥用、限流是否配置保护后端、回滚方案是否准备新版本出问题能快速切回。这份清单看起来繁琐但每一条都是踩过坑之后总结出来的。我见过太多团队因为没做压测上线当天就被流量打崩因为没配监控GPU 挂了半小时才发现因为没做限流被恶意请求刷爆账单。部署这件事细节决定成败。6.4 后续扩展方向如果你已经跑通了单模型部署下一步可以尝试多模型编排用网关做路由和限流。如果多模型也跑通了可以尝试自动扩缩容根据请求量动态调整实例数。再往上就是多租户隔离不同用户分配不同的 GPU 资源配额。最后是混合部署把 LLM、embedding、reranker 等不同模型混合调度到同一组 GPU 上最大化资源利用率。我个人在实际操作中的体会是部署框架的选择要匹配团队的技术栈和运维能力。vLLM 性能好但调优需要一定的 GPU 知识Triton 功能全但配置复杂FastAPI 简单但撑不住高并发。没有最好的框架只有最适合当前场景的框架。先跑通再优化不要一开始就追求完美架构。