
如果你的机柜里还躺着8张V100 32G而业务方又天天催着上大模型接口那这篇内容应该能帮你省下不少折腾时间。我们最近刚在一组8卡V100节点上把7B级别模型通过vLLM部署成了生产推理服务并且针对真实业务流量做了一轮完整的吞吐测试。整个过程不是跑个官方benchmark交差而是从请求构成、并发策略、延迟口径到瓶颈定位都重新捋了一遍。这篇文章就把这套部署参数、测试方法、实测数据和踩坑记录完整写下来给同样被旧卡绑住手脚的团队做个参考。先说结论8张V100 32G跑7B模型完全可行跑13B也能跑但余量很紧真正要花心思的不是“能不能跑起来”而是“在连续批处理和KV Cache的高效管理下怎么把吞吐和延迟调整到业务能接受的范围”。vLLM在这里的价值不是单纯的推理加速而是它把GPU算力、显存带宽和调度效率拧成了一条完整的链路这套链路放在V100这种老架构上反而比不少新框架更有优势。1. 先算账8张V100跑7B/13B的可行性边界1.1 显存和模型规模的硬约束V100 32G的显存容量放在今天看并不算寒酸但它的短板在架构上计算能力7.0支持FP16但不原生支持BF16HBM2带宽每卡约900GB/s。这意味着权重精度、KV Cache预留和模型并行方式都必须老老实实按FP16去规划。以7B模型为例FP16权重占用大约14GB如果单卡部署32G显存减去权重后还剩18GB左右给KV Cache和激活值。这个余量用来跑一个4K到8K上下文的服务是够的但并发一高KV Cache会成为第一个瓶颈。vLLM的PagedAttention在这里的作用非常关键——它把KV Cache拆成固定大小的块按需分配不像传统方案那样按最大序列长度预先锁死整块显存。我们在单卡先试跑时同样的7B模型传统推理框架大概只能同时服务十几个会话切到vLLM后同显存下的并发承载量能翻出好几倍。13B模型FP16权重约26GB单卡32G也能硬塞进去但KV Cache余量就非常紧张了只能靠缩小max_model_len或者降低并发来换。我们的生产环境最终选了7B模型原因不是跑不动13B而是13B在V100上的TPOT每Token生成延迟和KV Cache弹性都太勉强服务体验不稳定。如果一定要上13B建议切成2卡或多卡张量并行而不是单卡硬扛。1.2 vLLM到底解决了V100的什么痛点V100在LLM推理里最大的矛盾是模型权重太大一次推理要读一遍所有权重而HBM带宽决定了读取耗时的下限。传统批处理是等一个batch攒满再一起算遇到请求长短不齐就会大量浪费vLLM的Continuous Batching让请求动态进出解码阶段每步都能塞进新的请求来填满算力。这个机制对V100尤其友好。因为V100的算力放到今天不算顶级当并发上来后批处理粒度越细显存带宽利用率越高整体吞吐越接近硬件极限。再加上PagedAttention把KV Cache利用率拉高等于在显存总量不变的情况下变相提高了“有效Batch Size”。我们用8卡全TP张量并行跑7B模型时理论上每卡只用加载1/8的权重显存基本都留给KV Cache这意味着可以同时挂起非常多请求而不被OOM打断吞吐自然就上去了。2. vLLM部署参数复盘我最终定下的启动方案2.1 启动命令和关键配置说明部署环境是CUDA 11.8、PyTorch 2.1、vLLM 0.6.x系列。模型放在共享存储里8卡节点通过NVLink全互联。最终启动命令如下python -m vllm.entrypoints.openai.api_server \ --model /data/models/internal-qa-7b \ --tensor-parallel-size 8 \ --dtype float16 \ --max-model-len 8192 \ --gpu-memory-utilization 0.93 \ --max-num-seqs 256 \ --block-size 16 \ --enable-prefix-caching \ --enforce-eager \ --trust-remote-code \ --port 8000几个参数背后的考量--tensor-parallel-size 88卡全量张量并行模型权重分布式切分单卡显存压力最小KV Cache空间最大。换来的是跨卡通信开销但V100 NVLink在处理7B这种规模的激活同步时压力不大。--gpu-memory-utilization 0.93不要设置成0.95以上V100显存碎片问题比新卡明显我们实测0.96会偶发散CUDA OOM回退到0.93后连续跑了72小时没出问题。--max-num-seqs 256这个值决定了连续批处理最大能同时调度多少序列。V100单卡带宽有限256在8卡TP下是一个能跑满但不会让延迟失控的平衡点。--enforce-eager这个开关针对V100值得单独说。V100在CUDA Graph捕获时经常因为显存分配行为不稳定导致首次请求卡顿甚至报错我们线上环境直接采用Eager模式牺牲了一部分极端批处理效率换来了更稳定的调度行为。2.2 为什么必须把BF16排除在外V100的Tensor Core对FP16支持很好但对BF16没有原生支持。很多同学会在模型转换时顺手用BF16导出权重放到V100上要么被迫走模拟路径要么直接在运行时报错。我们当时吃过这个亏模型一开始是用BF16存的vLLM加载时虽然没立刻报错但推理速度明显异常日志里也有大量类型转换警告。后来统一用torch.float16重新导出一遍权重问题才彻底消失。如果你拿到的模型权重只有BF16版本建议先做一次权重转换再部署不要指望推理框架自动帮你无损解决。转换脚本不复杂核心就是按层加载后重新保存python convert_bf16_to_fp16.py \ --input /data/models/mix-bf16 \ --output /data/models/internal-qa-7b \ --dtype float16这一步看起来不起眼但在V100上属于“不做必然后续翻车”的步骤。2.3 张量并行规模不是越大越好8卡全TP是我们线上真实采用的方式但这里有个容易被忽略的点模型切分粒度越细KV Cache虽然是全局统一的但Attention计算是需要跨卡做AllReduce同步的。对于7B模型TP8的通信量尚可如果是13B模型TP8时的每层矩阵计算时间更长通信占比反而会下降效率会更好。反过来如果只跑一个很小的模型比如3B级别TP8就是纯浪费不如拆成两个独立服务实例。这里给一个实践结论模型参数量乘以权重字节数后单卡能放下并且有50%以上的显存余量给KV Cache时优先单卡或多个实例如果模型权重占用超过单卡显存80%才考虑上张量并行。我们选择7BTP8是因为要一次性承接过千路并发请求单卡实例根本吃不下这个量级的KV Cache占用。3. 吞吐测试方案如何模拟真实业务而不只是刷数字3.1 官方Benchmark的局限性vLLM自带的benchmark脚本适合验证环境是否调通但它不能直接回答一个生产问题业务高峰期进来1000个请求长短文本混杂模型响应到底能不能扛住官方压测通常会固定输入输出长度、固定并发数、提前把请求全部压进去这会让服务端处于一个理想的“最短队列”状态和真实生产差异很大。我们的业务场景是知识库问答和文档摘要请求串的特点是输入长度极不均匀有300Token的短问题也有3500Token的长文档输出长度从几十到上千Token不等。如果只压一种长度得出的吞吐数据会严重虚高。为此我们专门构造了一套混合请求集把输入长度分成几个区间每路压测请求从区间里随机抽取尽量模拟真实调用。压测环境用一台单独的客户端机器发起请求不走内网网关直连vLLM的8000端口避免网关层限流干扰数据。这样做不是为了测端到端性能而是为了把推理服务本身的吞吐和延迟看透。3.2 我们定义的三个核心指标很多团队汇报吞吐时只说“总Tokens数除以总时间”这个指标太粗。我们统一了口径生产看三组数一是整体吞吐单位是Tokens/s统计服务端实际生成的所有输出Token不包含输入的Prompt Token。二是TTFT也就是从发起请求到收到第一个输出Token的时间这个指标直接反映用户“感觉到开始回答”的等待时间。知识库问答场景里TTFT超过3秒用户会有明显卡顿感。三是TPOT即每个输出Token的平均生成间隔反映“吐字速度”。通过持续流式输出时每两个Token的到达间隔来估算也可以用总输出时长除以总Token数近似。TPOT乘以输出长度约等于用户从开始到完结的总等待时长。压测脚本用Python异步客户端实现核心逻辑是对每一路压测流记录请求发出时间、首Token到达时间、完整响应结束时间然后按并发分组汇总。我们压测了6个并发档位16、32、64、128、256每个档位持续跑15分钟确保服务端进入稳定状态后再取样。注意并发数指“同时在服务端排队和生成中的请求数”不是每秒发起的请求数。我们固定保持目标并发数一个请求结束立刻补一个新请求这样测的是服务端稳定承载能力而不是客户端发压速度。3.3 关于Prefill和Decode混合负载的观察真实业务里Prefill处理输入和Decode逐Token生成是混杂在一起的。长文档请求进入时一次Prefill可能产生几千KVCache占用连续批处理调度器会暂时挤占其他请求的Decode时间。我们特别观察了请求队列里混20%长文档时TTFT会不会被明显拉高。跑完后发现vLLM的调度器对长Prefill有专门的抢占机制但抢占本身在V100上比较重因为它需要把部分KV Cache换出。如果你的业务里频繁出现长文档并发进入建议在网关层做请求排队限流让同时进入服务的长Prefill数量不超过一个阈值否则服务虽然不崩但TPOT会周期性跳到离谱的高度。4. 实测性能画像吞吐、并发与延迟的三方关系4.1 各并发档位下的数据汇总下表是我们用混合请求集得出的实测数据模型为7B FP16TP8max_model_len8192gpu_memory_utilization0.93。请求输入的Token长度分布为40%在500Token以内30%在500到1500Token之间20%到3000Token10%在3000Token以上。并发数整体吞吐Token/s平均TTFTsP95 TTFTs平均TPOTs/TokenP95 TPOTs/Token1613800.450.820.0420.0653223500.631.350.0580.0886435601.052.100.0760.12012848201.853.200.1040.17525656303.406.500.1550.310单从吞吐看并发从128增加到256整体吞吐只涨了17%左右但TTFT和TPOT的劣化非常明显。这就是V100这类带宽受限GPU的典型特征并发越高批处理越大单位Token需要的带宽分摊确实在减少但排队和调度延迟会开始吞噬收益。我们的业务SLA要求P95 TTFT不超过3秒P95 TPOT不超过0.2秒所以最终把生产并发上限锁在了128左右而不是去追求峰值5630Token/s。另外要注意这个整体吞吐是包含了大量短输出的请求。如果业务里输出长度普遍很长整体Token吞吐会上升但从用户视角看每个请求的响应时间并不会因此变快所以汇报时一定要把“系统吞吐”和“用户感知延迟”分开讲。4.2 短输出请求对吞吐的“拉高效应”混合负载里有一类请求输出只有几十个Token这类请求在连续批处理里非常受欢迎因为它们占用KV Cache时间短调度器可以快速把它们清出队列腾出空间给新请求。我们单独测了一组纯短输出请求发现同样128并发下整体吞吐可以超过6200Token/s比混合负载高接近30%。这个现象说明如果只看每秒生成Token数很容易被业务构成骗了。我们内部汇报时会把“短请求占比”列出来方便决策层理解为什么同样一套服务不同时段吞吐差异会那么大。这也是为什么压测必须用贴近真实业务的请求分布而不是一组固定长度。4.3 Prefill冲击下的延迟毛刺长文档混入时延迟曲线会出现非常明显的周期性毛刺。最典型的情况是一个4000Token的文档进入后Prefill阶段要消耗大量显存带宽和算力同一时刻正在Decode的请求会被拖慢TPOT从0.1秒跳到0.3秒以上。影响程度取决于长Prefill发生频率。后来我们在网关层加了一个简单策略长请求超过2000Token进入时先做限速保证同一时刻最多只有1到2个大请求在Prefill阶段。改造后P95 TPOT从0.35秒降回收敛状态。这个优化和vLLM无关纯粹是业务接入层的配合但对V100这种余量不大的硬件非常管用。5. 瓶颈到底在哪从数据反推V100的硬件极限5.1 Decode阶段被HBM带宽卡死V100的理论显存带宽是每卡约900GB/s8卡加起来约7.2TB/s但实际能跑到标称值的75%到85%就不错了。7B模型FP16权重14GBTP8切分后每卡只存约1.75GB权重。Decode阶段每生成一个Token理论上需要把当前卡上的权重读一遍依赖局部计算后通过AllReduce汇总。单卡读1.75GB权重的耗时按750GB/s有效带宽算是2.3ms左右加上通信和计算单并发下每步生成低于3ms是极限。但为什么我们测到的单并发TPOT在40ms左右因为vLLM默认会做批处理优化单并发时不会把算力跑满且连续批处理的填充粒度、Eager模式下的算子开销都会拉低效率。当并发到了64批大小上来后权重的读取被所有并发请求分摊单位Token成本才会下降这也是为什么整体吞吐能一路涨到5000Token/s以上。换个更直白的话说V100的Decode瓶颈不是浮点算力而是“把权重搬进计算单元”的内存带宽。只要并发数量够算力是可以被喂饱的但带宽是物理上限8张卡总的搬运能力就摆在那。5.2 Prefill阶段则是另一个计算画像Prefill阶段主要做长序列的矩阵乘属于计算密集型。7B模型处理1000Token输入的Prefill大概需要2倍的模型FLOPs也就是1.4万亿次浮点运算。V100的FP16 Tensor Core峰值约125TFLOPS单卡理论需要0.11秒8卡一起算则需要约14毫秒。看起来很快但这是理想值实际因为数据搬运、算子启动、Attention计算整体Prefill耗时至少是理论值的3到4倍。所以长文档并发进入时Prefill会明显抢占带宽这就是上一节看到的周期毛刺来源。解决思路也很直接不要让多个大Prefill同时发生必要时宁可让部分长请求排队等待也不要让它们一起涌入。5.3 TP8通信开销占实际比例我们一开始担心TP8的AllReduce会把性能拖垮实际测下来在NVLink环境下7B模型的通信开销可控。注意力层的KV同步和MLP层的AllReduce总通信时间大约占Decode步长的10%到15%。但这里有个前提8卡必须通过NVLink高速互联。如果你的8张V100是PCIe互联TP8的通信延迟会明显放大强烈建议改成2个TP4实例或者4个TP2实例否则吞吐和延迟都会吃亏。用nvidia-smi topo -m看一眼P2P链路如果发现CPU占用了跨卡路径就可以基本判定这是PCIe拓扑谨慎使用高TP值。我们这台节点比较幸运8卡之间有完整的NVLink矩阵所以TP8通信开销可控。6. 部署运维避坑与升级决策建议6.1 长期运行最需要注意的三个问题第一是GPU显存碎片。vLLM长时间运行后虽然PagedAttention在管理KV Cache但权重激活区和CUDA上下文仍可能产生碎片。我们遇到过连续跑48小时后出现加载新模型权重失败的情况重启服务后恢复。建议在监控里盯住每个CUDA进程的显存使用量发现异常攀升就计划性重启。第二是采样参数对吞吐的影响。业务方如果在请求里带了过大的max_tokens比如2048会让一批慢请求长期占据连续批处理的坑位影响整体吞吐。我们在网关层强制把max_tokens上限设成了1024同时把业务侧的超时时间对齐到模型能力范围这样既保护了服务也避免了单请求拖垮全队的调度。第三是模型权重和Tokenzier的一致性。生产环境出过一次“阶段性输出乱码”的故障排查到最后是Tokenizer文件更新了但模型权重没同步换导致采样结果偏移。这类问题在自建模型里很常见建议部署时把模型权重和Tokenizer版本做一次哈希校验记录到发布单里。6.2 V100节点是不是该淘汰了从这次测试数据看8张V100 32G的7B推理服务在128并发下能稳定输出将近5000Token/s这个量级已经能支撑一个中型团队内部的知识库问答、工单摘要、代码补全等业务。如果预算不允许马上换卡这个方案完全有继续服役的价值。但如果业务要升级到70B级别模型V100就不合适了。70B模型FP16权重140GBTP8后每卡仍要17.5GBKV Cache几乎没有空间而且V100不支持BF16要用FP16跑70B显存和带宽都会非常痛苦。到这一步再考虑换H系列或L40S等新卡才算正确的升级时机。另外一个小技巧8张V100如果要服务多个模型不一定每次都用全部GPU。可以用CUDA_VISIBLE_DEVICES把卡分组比如切成两组TP4同时部署两个7B服务这样单服务故障不会拖垮全部业务。我们在非高峰期就是这么干的让一组卡跑在线问答另一组卡跑离线批量生成资源利用率反而更高。最后补充一个实际运维里的体会V100这种老卡上的推理服务调参思路和A100/H100完全不是一回事。新卡显存大、带宽高很多参数可以往激进的区间调性能掉一点看不出什么V100则每一步都贴着硬件极限走参数稍高就会出现延迟毛刺稍低又会浪费算力。我们的做法是把监控粒度细化到“每30秒一个采样点”持续观察TPOT和TTFT的分布用两周时间一点点把参数修正到当前业务负载的最优解。这套方法比单纯跑一次压测定终身实用得多尤其适合硬件条件有限的团队长期运营。