我一开始用PyTorch把模型拉起来能跑、能返回结果,就以为部署这事结束了。直到一次线上活动压测,7B模型单卡吞吐死活上不去,尾延迟还忽高忽低,我才意识到"能推理"和"能扛住推理"完全是两码事。那段时间我把vLLM、SGLang、TensorRT-LLM挨个试了一遍,才算把"AI推理引擎"这件事从概念落到了地上。
这篇不是学术论文,也不是官方文档翻译,就是把我从PyTorch原生推理一步步迁到专用推理引擎的完整思考、踩坑和选型逻辑写出来。适合刚接触大模型部署、正在犹豫该用哪个引擎、或者已经被显存和CUDA折磨到怀疑人生的朋友。看完你会发现,选引擎没有标准答案,但每一条路都有自己的脾气。
1. 从一次失败压测开始:PyTorch原生推理到底慢在哪
先说那次翻车。我部署的是一个7B对话模型,按之前做小模型的经验,直接用PyTorch写了一个FastAPI服务,模型加载到GPU,请求进来就model.generate()。单请求测试一切正常,响应速度也不错。
压测工具一上,问题全暴露了。并发20路请求同时进来,GPU利用率只有百分之十几,显存倒是占得满满当当,可每秒生成的token数少得可怜。我把进程关了,一个个排查,最后定位到三个瓶颈,这三个瓶颈恰恰就是后面所有推理引擎想解决的问题。
1.1 动态图执行和kernel launch开销
PyTorch默认是eager模式,也就是每跑一行代码,Python解释器就调用一次底层算子,每个算子又对应一次GPU kernel启动。一次推理可能要执行几百个算子,每个kernel launch本身就有微秒级的固定开销。单请求的时候这开销不显眼,并发一上来,CPU忙不过来了,GPU就在那里等着CPU喂数据。
这不是PyTorch一家的问题,是动态图框架的通病。PyTorch后来推出torch.compile就是想解决这个事,把多次kernel启动合并成少数几个大的kernel。效果确实有,但torch.compile的编译时间感人,而且碰上动态shape或者某些自定义算子,优化效果会大打折扣。更关键的是,torch.compile只解决了算子调度问题,下面两个瓶颈它管不了。
1.2 KV Cache的缺失与显存的浪费
大模型生成是自回归的,每生成一个新token,都要重新计算前面所有token的注意力。没有KV Cache的话,历史token的Key和Value每次都要重新算,算力全浪费在重复计算上。
我手工实现了KV Cache,吞吐确实上来了,但显存管理又成了问题。PyTorch分配显存是贪心的,预先申请一大块,之后不够了再扩容。每个请求进来的sequence长度不一样,KV Cache的大小也不一样,显存碎片很快就出现。最痛苦的是,所有请求都独占自己的缓存,不能共享。一个请求结束,它的缓存区域就空着,等下一个请求来申请差不多大的空间才能复用。这个浪费程度,压测时我盯着nvidia-smi看,心都在滴血。
1.3 并发调度的短板
FastAPI并发靠协程,但model.generate()本身是同步阻塞的,并发请求实际是排队执行的。GPU一次只能跑一个batch,哪怕这几个请求完全可以用不同的prompt拼在一起跑,我也没有一个自动的机制去拼。
理论上我可以自己实现动态batch:收集一段时间内的请求,一起forward。但这涉及到padding、显存分配、超时控制一堆问题,自己写的话至少得花一两周才能稳定。更别提请求长短不一,短请求得等长请求跑完才能释放显存,这个"队头阻塞"让你就算做了动态batch,利用率还是上不去。
所以那次压测之后我基本想明白了:原生PyTorch不是不能做推理服务,但要把工程化做好,你其实是在重复造轮子的路上。vLLM、SGLang、TensorRT-LLM这些专用引擎,恰好就是把这些轮子都造好了,等你来用。
2. vLLM、SGLang、TensorRT-LLM的分工逻辑:谁真正解决了什么问题
这三个引擎我依次用过,先说结论,它们解决的不是同一个问题。vLLM解决的是"吞吐和服务化",SGLang解决的是"复杂prompt结构下的前缀复用",TensorRT-LLM解决的是"NVIDIA硬件上的极致榨干"。把它们放一起比谁快谁慢,其实是问错了问题。
2.1 vLLM:PagedAttention和连续批处理
vLLM的核心是PagedAttention,灵感来自操作系统虚拟内存的分页机制。传统KV Cache是一段连续显存,PagedAttention把KV Cache切成固定大小的块,用块表来管理。这解决了两个问题:显存碎片化几乎消失,而且不同请求之间部分块还能共享。
加上continuous batching(连续批处理),vLLM不再等一个batch全部跑完才接下一批,而是有请求完成了就立刻腾出位置,把排队的新请求插进去。同样是30个并发请求,PyTorch手写动态batch可能只能维持一个批次,vLLM会在微秒级别动态调整batch成员,吞吐就是这么堆上去的。
我实测Qwen2.5-7B,同样的单卡A100,PyTorch原生大概每秒十几到二十个token的总吞吐,vLLM直接到每秒几百token,提升不是一倍两倍,是十倍以上。如果你的需求是"尽可能多的并发用户能用上模型",vLLM是这三个里最省心的选择。
2.2 SGLang:RadixAttention和结构化推理
SGLang和vLLM的功能有大量重叠,很多人纠结二选一。它的独特之处是RadixAttention,会维护一个前缀树(radix tree),把每个请求的prompt拆成前缀,多个请求重复的前缀只存一份KV Cache。
这个特性在什么场景下收益最大?多轮对话、few-shot prompt、批量评测、Agent工具调用。这些场景的共同点是大量请求共享相同的前缀,比如每轮对话都要重新带上System Prompt和历史消息。vLLM也能做prompt prefix caching,但SGLang的前缀树粒度更细,缓存命中率更高,实测在Agent类场景下首token延迟能低不少。
不过SGLang在纯单请求连续生成的场景下,和vLLM差距并不明显,有些benchmark里还略慢一点。所以我的习惯是:普通在线对话服务,选vLLM;Agent链路、大量前缀复用、需要灵活控制prompt结构的,试试SGLang。
2.3 TensorRT-LLM:编译期的极致优化
TensorRT-LLM的思路和前两者完全不同。vLLM和SGLang本质还是"运行时的调度优化",TensorRT-LLM是"编译期的结构优化"。它把整个模型经过ONNX导出等流程转换成一个TensorRT engine,也就是一个针对你具体GPU型号、具体batch规模、具体精度预先做了一堆融合和内存规划的plan文件。
好处是推理时省掉了大量运行时的判断和调度,kernel融合更彻底,还能用上NVIDIA各代硬件专属的算子。配合INT8、FP8量化,能把更小的显存放更多模型、跑更高吞吐。坏处也很明显:建engine很慢,换一个batch size或者换一块GPU都要重新build,调试排查问题难度直线上升。
我的经验是,TensorRT-LLM适合"模型固定、硬件固定、参数固定"的生产环境,比如上线后不怎么变的单模型服务。如果你还在频繁换模型试效果,用TensorRT-LLM会把自己折磨死。
三个引擎最直观的对照如下:
| 维度 | vLLM | SGLang | TensorRT-LLM |
|---|---|---|---|
| 核心优化点 | PagedAttention、连续批处理 | RadixAttention前缀复用 | 编译期kernel融合与内存规划 |
| 优势场景 | 高并发在线服务 | Agent、多轮对话、前缀复用 | 固定模型极致吞吐与低显存 |
| 易用性 | 高,OpenAI兼容API | 较高,风格接近vLLM | 低,需要build engine |
| 硬件要求 | 不限品牌,NVIDIA最佳 | 不限品牌,NVIDIA最佳 | 仅NVIDIA |
| 量化支持 | 有但不深 | 有但不深 | INT8/FP8深度优化 |
3. 10分钟落地一个推理服务:vLLM启动Qwen2.5的完整记录
说那么多概念,不如直接跑一个。现在从头到尾用vLLM部署一个Qwen2.5-7B-Instruct服务,整个过程如果环境正常,十分钟足够了。我把每一步的意图和坑也写出来,免得你照着敲完不知道发生了什么。
3.1 环境准备:一个不小心就翻车的conda细节
先说一个我见得太多的翻车现场:Windows上打开PowerShell,输入conda activate vllm,然后报错"无法将conda项识别为cmdlet、函数、脚本文件或可运行程序的名称"。这不是你装错conda,而是conda的shell初始化没做。
解决方法是先把conda初始化到当前shell,在PowerShell里执行conda init powershell,然后重新打开终端。如果还是不行,就用Anaconda Prompt代替PowerShell,或者直接用conda完整路径:C:\Users\你的用户名\anaconda3\Scripts\conda.exe activate vllm。
然后是最关键的一个问题,很多人问"安装vllm会改变已经安装好的torch吗"。答案是:会,而且经常出问题。pip安装vllm时会自动检查依赖,如果环境里已有的torch版本不满足vllm的要求,它会把torch升级或降级。如果你的训练代码依赖旧版torch,升级后轻则行为变化,重则直接ImportError。
所以铁律是:给vllm单独建一个conda环境,绝不和训练环境混用。
conda create -n vllm python=3.10 -y conda activate vllmPython版本选3.10或3.11比较稳妥,vllm对这些版本的支持最成熟。接下来安装vllm。如果只是部署推理,不需要整个pytorch体系,vllm安装时会自动把配套的torch装上。
pip install vllm这一步会下载不少东西,包括编译好的CUDA算子,耐心等就行。安装完可以用python -c "import vllm; print(vllm.__version__)"验证。
3.2 启动命令与关键参数
模型文件可以用HuggingFace官方权重,也可以先用huggingface-cli download下载到本地再启动,避免每次启动都去线上拉权重。国内网络环境下载HuggingFace经常慢到怀疑人生,建议直接用ModelScope的镜像或者国内加速方式把权重提前下好。
模型就位之后,命令行如下:
vllm serve Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --port 8000解释一下这几个参数,它们是新手最容易犯错的地方。
--tensor-parallel-size是张量并行度,单卡就写1,多卡就写卡数。注意它要求显存足够装下完整模型,否则会OOM。比如7B fp16的权重约14GB,单张24GB显存的卡能跑,单张16GB显存就很紧张,得配合量化或减小max-model-len。
--max-model-len是最大上下文长度,包括输入和输出。这个值直接决定KV Cache占多少显存,设得越大显存占用越高。如果只是测试,建议先用4096或2048,跑通后再按需调大。
--gpu-memory-utilization是vLLM最多用多少比例显存,默认0.9,也就是90%。如果你的GPU还要跑其他任务,调低这个值。反之,如果GPU整卡只给这个服务,0.9是个合理值,留出的10%给CUDA context和临时变量。
启动成功后日志会显示模型加载耗时、GPU内存预算、KV Cache池大小等信息。vllm会自动生成一个OpenAI格式的API,类似http://localhost:8000/v1/chat/completions。
测试一下:
curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model": "Qwen/Qwen2.5-7B-Instruct", "messages": [{"role": "user", "content": "用一句话解释什么是KV Cache"}], "max_tokens": 128}'能返回正常文本,就说明部署成功了。Python里用openai库指向这个地址也能正常调用,迁移成本几乎为零。
3.3 服务化之后的吞吐验证
我的习惯是部署完先用一段简单脚本压一下,确认吞吐符合预期,再交给业务方。脚本逻辑不复杂:并发开20个客户端,每个发一个请求,统计完成时间和生成的token数。
实测下来,单张A100部署Qwen2.5-7B,max_model_len8192,并发20,输出速度稳定在每秒40到60 token的体验速度,总吞吐可以达到每秒几百token。对比之前PyTorch原生部署,同样条件下的总吞吐提升大约是十倍量级。如果你部署后GPU利用率仍然很低,优先检查是不是tensor-parallel-size设错、显存利用率设太低,或者请求太小导致batch还没攒起来。
4. 换引擎路上的深坑实测:NVLink、engine转换、显存预算与CUDA版本
跑通vLLM只是开始。真正让很多人放弃的,是换引擎、多卡、量化这一路上的暗坑。我把自己踩过的坑按照从浅到深写一遍,每一条都是真金白银换来的。
4.1 无NVLink的多卡环境,影响到底有多大
很多人的机器是两台双路服务器插了4张GPU,但GPU之间没有NVLink桥接,只有PCIe互联。这时候跑张量并行,心里就会打鼓:没有NVLink,性能是不是废了?
先说结论:小模型、小并行度,影响不大;大模型、高并行度,影响非常明显。
NVLink的双向带宽在最新几代产品上可以到600GB/s甚至900GB/s,而PCIe 4.0 x16的单向带宽约32GB/s,双向约64GB/s。差距接近一个数量级。SGLang或vLLM在做Tensor Parallel时,每一层transformer的中间结果都需要跨卡做AllReduce,通信量跟模型宽度相关。7B这种规模的模型,TP=2的时候,PCIe的通信时间占总计算时间的比例还能接受,实测吞吐损失大约10%到20%。
到了70B甚至更大模型,TP=8环境下,PCIe根本喂不饱计算单元,吞吐损失能到50%以上。遇到这种情况,优先考虑减少并行度,或者改用Pipeline Parallel。把模型层切分到多卡,卡间只需要传递activation和梯度,通信量远小于Tensor Parallel。如果模型大到单卡放不下,而后台硬件又没有NVLink,尽量选择PP或者EP(专家并行),而不是硬上大规模TP。
4.2 TensorRT-LLM的engine构建,真的会把人逼疯
TensorRT-LLM的核心流程是:拿到HuggingFace权重,转成engine,然后用engine跑推理。听起来简单,实际比vLLM麻烦不少。
vLLM启动时直接加载权重,TensorRT-LLM要先跑一次trtllm-build,把整个模型编译成plan文件。编译期间会做层融合、算子选择、内存规划,所以编译时间从几分钟到几十分钟不等。麻烦的是plan文件和GPU型号强相关,在A100上build的engine放到4090上不一定能跑,甚至CUDA版本和TensorRT版本变了都要重新build。
我做FP8量化的时候更痛苦。TensorRT-LLM支持INT8、FP8、INT4等量化方式,但量化校准这一步非常考验经验。不是所有模型都能平滑量化,量化后精度掉多少需要拿业务数据实测。我试过对某些模型做INT8量化,推理速度确实快,但回答质量明显下降,最后还是得回到FP16。
用TensorRT-LLM的前提是你要接受一个事实:你的部署流程里会多出一个不可跳过且高成本的"编译阶段"。如果你的模型和服务是长期稳定的,这个编译成本摊下来完全值得。如果你每周都在换模型版本,建议还是用vLLM这种运行时加载权重的方式。
4.3 显存预算的快速算法与DeepSeek这样的大模型
显存是推理服务最贵的资源,提前算清楚再启动可以省很多事。
模型权重的显存大概估算:模型参数量乘以每个参数的字节数。FP16是2字节,7B模型就是14GB,70B就是140GB。这还没算KV Cache和激活值。
KV Cache的显存估算公式是:2(K和V)* 层数 * KV头数 * head_dim * 序列长度 * 2字节。拿Qwen2.5-7B为例,假设30层、4个KV头、head_dim 128,序列长度8192,单请求的KV Cache约2 * 30 * 4 * 128 * 8192 * 2 / 1024 / 1024 / 1024 ≈ 0.47GB。看起来不大,但如果是并发128个请求,就是60GB。所以vLLM的gpu-memory-utilization参数才会这么关键,它直接决定了KV Cache池能容纳多少并发。
DeepSeek这类MoE大模型需要单独说。DeepSeek-V3/R1总参数量671B,但每次激活的专家参数只有37B左右,所以它的显存压力来自两部分:所有专家权重都要驻留在显存里,以及庞大并发下的KV Cache。部署这种模型通常需要多卡甚至多机,且强烈依赖专家并行(EP)。EP会把不同专家分布到不同卡上,每张卡只需要算被路由到的专家。这时候卡间通信非常频繁,NVLink的价值比小模型场景大得多。如果你打算用vLLM部署DeepSeek系列,建议先查官方文档确认当前版本对EP和长上下文的支持情况,同时把显存预算按真实并发仔细算一遍。
4.4 CUDA、驱动和pip的版本错配,是高发翻车区
vLLM、SGLang、TensorRT-LLM都依赖特定CUDA版本和PyTorch/cuBLAS版本。最典型的报错是:启动时提示CUDA driver version is insufficient,或者找不到某个cuDNN符号。
排查顺序先看驱动:nvidia-smi里显示的CUDA Version是驱动支持的最高版本,比如12.4。再看容器或conda环境里nvcc --version,这是PyTorch编译时用的CUDA版本。这两个版本不一致不一定会崩,但官方通常要求PyTorch的CUDA版本不高于驱动支持的版本上限。
vLLM对CUDA版本很敏感,不同版本对GPU架构的支持也不同。比如某些旧版本不支持Ada Lovelace(RTX 4090、L40S),需要升级到较新版本。TensorRT-LLM更是每个版本都绑定特定TensorRT版本和CUDA版本,换版本很容易引入新的兼容性问题。
我现在的做法是,尽量用官方发布的Docker镜像作为部署环境。NVIDIA和vLLM官方都维护了已经配好CUDA、cuDNN、TensorRT的镜像,比自己折腾环境省事很多。如果公司不让用Docker,那至少把CUDA版本和驱动版本记录下来,每次升级引擎版本前都去官方文档核对一遍最低要求。不要问"为什么昨天好好的今天就不行了",先查"昨天我改了什么"。
5. 选型判断与个人倾向:给准备上推理引擎的人一份实在建议
三个引擎都跑过一遍之后,我对选型有了一套自己的判断逻辑。先说一个核心观点:不要为了追求基准测试的分数而换引擎,要为了你的具体场景换引擎。
如果你的场景是Chatbot、RAG问答、API服务这类高并发的通用对话,vLLM是最稳的选择。生态最成熟,社区最活跃,遇到问题搜一搜基本都有答案。SGLang在Agent场景和多轮对话上有优势,但它的社区和文档成熟度比vLLM稍弱一点,调试起来需要更多耐心。
如果你对首token延迟要求极高,而且请求带有大量重复前缀,比如"系统提示词+历史消息列表",SGLang值得认真测一测。它在这些场景下对首token延迟的优化明显,个别benchmark里能比vLLM快20%以上。
如果你有长期稳定的NVIDIA GPU集群、模型和参数不怎么变、还想在量化上压榨极致性能,TensorRT-LLM是值得投入的。投入产出比在"模型固定、规模大、并发高"的条件成立时非常可观。但你要先接受build engine的时间和调试成本。
如果你正在部署超大模型,比如几百B级别的MoE,vLLM和SGLang是当前更现实的选择,因为它们对EP、动态调度、长上下文支持更灵活,迭代也更快。TensorRT-LLM虽然也能跑,但工程链路长、版本兼容性复杂,除非有专门的推理团队维护,否则不建议新手直接上手。
最后分享一个我的实际体会:推理引擎的吞吐数字只能代表benchmark,不能代表你的业务。跑通一个引擎的最小流程不难,难的是把你的业务特征、模型特征和硬件特征对齐。先想清楚自己的场景最痛的点是什么,再动手选引擎。这样就算踩坑,也是踩在对自己有回报的路上。