最近在给业务线搭大模型推理服务手里能用的卡只有8张V100 32G。说句实话一开始我挺排斥这批卡的总觉着V100是上一代的产物跑跑训练还行拿来给大模型做线上推理多少有点勉强。但真正折腾完这一轮部署和压测之后我的看法变了不少V100 32G虽然没有NVLink的带宽优势那么夸张但配合vLLM的PagedAttention和Continuous Batching完全可以在真实业务场景里撑起一个可用的大模型推理服务。这篇内容就是把我这次从零开始到完成8卡V100 32G上vLLM部署、再到业务吞吐测试的完整过程记录下来。包括环境检查、模型选型、vLLM关键参数设置、压测脚本怎么设计、实测数据长什么样还有我在这个过程中踩过的一些坑。如果你手里也是V100这种老卡或者正在评估存量GPU资源能不能扛住大模型业务这篇内容应该能帮你少走不少弯路。1. 项目背景与整体思路拆解1.1 为什么拿V100 32G做推理要说清楚这个项目得先聊聊为什么还要用V100 32G这种卡来做大模型推理。这两年新卡层出不穷A100、H100甚至更新的型号都把显存和带宽拉得很高。但现实情况是很多公司和实验室手里并没有那么多新卡可用尤其是在存量机房中V100 32G仍然是相当常见的一种资源。V100 32G有几个很实在的优势一是显存32G虽然比A100的40G/80G小但在多数开源大模型的FP16权重部署场景下勉强够用二是V100的FP16算力不错官方数据是112 TFLOPSTensor Core纯推理状态下并不算太弱三是SXM2版本的V100支持NVLink虽然没有A100的带宽那么猛但卡间通信不是完全不可用。这次项目用的就是8张V100 32G的SXM2版本配了NVLink互联。在做推理测试之前我先用nvidia-smi topo -m确认了卡的拓扑结构确保8张卡是完整连接、没有PCIe降级的情况。实测下来NVLink在张量并行场景中确实能发挥一定作用但相比A100的600GB/s带宽V100的NVLink 300GB/s只是够用而已。这一点在后面对吞吐的影响分析中会再次提及。1.2 vLLM为什么是首选推理框架确定硬件之后接下来就是框架选型。当前主流的开源推理方案有vLLM、TGI、TensorRT-LLM等。TensorRT-LLM性能很强但配置复杂尤其是在老卡上做量化、编译调试成本偏高TGI部署简单但灵活度和性能弹性不如vLLM。综合比较之后我选择了vLLM核心原因有三点。第一vLLM的PagedAttention机制能极大减少显存碎片和KV Cache浪费。简单类比就像操作系统的虚拟内存分页把连续的KV Cache物理块拆成多个page按需分配显存利用率提升非常明显。对于显存仅有32G、还要同时塞下多路请求的V100来说这一项优势几乎是决定性的。第二vLLM自带Continuous Batching也就是连续批处理。传统的静态batching要等一个batch所有请求都生成完才释放资源而vLLM在每一步decode时动态调度新请求进入吞吐上限能够提高数倍。这对真实业务场景特别重要因为实际请求不是整齐划一的而是长短错落、随意到达的。第三vLLM提供了兼容OpenAI API的服务接口业务端接入成本极低。我这次的项目就是直接把它当作一个后端推理服务暴露给业务方用户侧只需要简单的HTTP请求就能获得大模型能力。当然vLLM也有它的适用范围。如果追求极致吞吐、且你有大量时间和人力去优化算子TensorRT-LLM会更合适如果只是简单试用TGI也足够。但考虑到团队需要快速交付、同时保留调优空间vLLM确实是当前最务实的选择。2. 部署前的硬件环境与模型选型2.1 硬件基线检查清单在安装vLLM之前先把硬件基线确认清楚。这里我给出一份我当时整理的检查清单每一条都值得认真过一遍。GPU识别情况通过nvidia-smi确认8张卡全部正常、无掉卡、无ECC错误。驱动与CUDA版本驱动版本建议不低于470CUDA Toolkit使用11.8或12.1均可但vLLM编译版本需要和实际CUDA环境匹配。NVLink拓扑检测执行nvidia-smi topo -m查看卡间互联关系确认SXM2的NVLink全连接状态。系统内存vLLM加载模型时需要把权重读入内存7B级别的模型大约需要15GB左右内存系统内存建议64GB以上否则加载容易失败。磁盘IO模型文件通常几十GB需要保证模型所在磁盘具备一定的读取速度。SSD是标配如果还在用机械硬盘加载模型的时间会让人崩溃。我这台机器的系统是Ubuntu 20.04Python 3.10CUDA 11.8。在此版本组合下vLLM的预编译包可以直接安装不需要从源码编译减少了不少调试时间。2.2 模型选型与量化策略明确环境之后开始选模型。这里的核心约束是显存和算力。8张V100 32G总显存256GB看起来很大但要注意TP并行下的显存分布和KV Cache预留实际可用显存并没有想象中那么充裕。本次项目我最初选了一个7B规模的开源基座模型FP16格式权重大约14GB-16GB。如果采用TP8单卡只需承载大约2GB的权重剩余大量显存给KV Cache这样并发能力理论上会非常可观。为了对比我也测试了13B规模的模型FP16权重约26GB-28GBTP8后单卡承载3.5GB左右权重KV Cache空间依然够用但对吞吐有明显影响。这里有一个重要细节V100架构不支持BF16计算。很多新模型官方权重是用BF16格式发布的V100并不支持直接推理。我在测试初期就踩过这个坑加载某个BF16权重直接报错后来统一转为FP16才跑通。如果你的卡是V100务必确认模型权重的精度格式BF16权重需要先做格式转换。量化策略方面我试过FP16和INT8两种方案。V100对INT8的Tensor Core支持不算强但GPTQ或AWQ量化的模型在V100上确实能跑而且显存占用明显下降。不过INT8量化后模型输出质量会有一定波动尤其对逻辑推理类任务影响比较明显。个人建议如果显存足够且业务对输出质量要求较高优先使用FP16如果业务量很大、需要极限压缩显存可以尝试INT8 AWQ量化同时做充分的效果回归测试。2.3 张量并行度的选择TP并行度是部署中一个非常关键但又容易被忽略的参数。8张卡的常见选择是TP8或者TP4开两个独立服务实例。这两种方案各有优劣。TP8意味着模型权重的每一层都会被切分到所有8张卡上每个请求的所有计算都需要卡间通信。对于V100来说NVLink带宽300GB/s虽然不算差但相比计算速度仍然是瓶颈。TP8的优势是单实例能获得最大显存和最强算力适合超大模型或者长上下文场景。TP4开双实例则不同每个实例独立处理请求完全不需要跨实例通信。这在两个模型实例分别承担不同业务时更灵活同时单个实例内的通信开销也更小。但TP4时每个实例只有4张卡显存和算力都被减半如果模型较大KV Cache空间会明显缩水。我这次采用的是TP8单实例方案原因很简单业务方需要处理的模型只有一个且模型并不算太大单实例TP8可以获得最低的延迟同时简化推理资源的管理。如果你有多业务隔离需求TP4双实例会是更合适的选择。这里没有绝对正确答案核心是看你的业务形态和并发需求。3. vLLM部署与核心配置解析3.1 部署方式选择vLLM的部署方式主要分为源码编译和预编译包安装两种。预编译包安装最简单pip install vllm但需要注意预编译包通常对应特定CUDA版本和PyTorch版本如果你本机的CUDA环境不一致会碰到各种动态库缺失的报错。本次我使用的是CUDA 11.8环境安装了指定版本的vLLM整个过程很顺利。源码编译适合需要修改底层算子、或者需要启用特定硬件特性的人群。对于V100这种成熟显卡预编译包已经足够。不建议一上来就源码编译因为LLVM、PyTorch、CUDA版本之间的依赖关系会让调试时间直线上升。部署时还推荐使用Docker。vLLM官方提供镜像拉下来即可运行避免污染宿主机环境。我这次用的命令大概长这样docker run --runtime nvidia \ --gpus all \ -v /data/models:/models \ -p 8000:8000 \ vllm/vllm-openai:latest \ --model /models/your-model \ --served-model-name your-model \ --tensor-parallel-size 8 \ --gpu-memory-utilization 0.92使用Docker的好处是升级、回滚、多版本验证都方便。如果后续要部署多个模型实例只需要换端口和模型启动参数即可。3.2 启动参数逐个拆解vLLM的启动参数非常多这里挑几个对V100部署影响最大的参数展开说明。--tensor-parallel-size张量并行度。8卡环境一般直接设置为8。若单卡显存足够、或并发压力不大可以设为4通过多实例分摊流量。--gpu-memory-utilization控制模型加载后剩余显存中可以分给KV Cache的比例。默认是0.9我调成了0.92。这个参数不是越大越好至少要预留一些显存给临时变量和碎片余量。V100 32G单卡模型权重约14GBTP8后单卡约2GB剩余30GB可用于KV Cache0.92意味着约保留2GB左右的冗余比较稳妥。--max-model-len最大序列长度输入输出。这个参数直接决定KV Cache的分配上限。设太长会让显存占用暴增设太短会导致超长请求被拒绝。本次业务场景基本在2K以内我设为4096既能覆盖线上业务又不会浪费显存。如果你的业务偶尔出现超长输入可以适当调大但要关注显存占用与后续并发能力之间的平衡。--max-num-seqs一次batch内最多同时处理的序列数。默认是256。这个参数对吞吐的影响很大。如果设得太高显存可能不够用设得太低并发能力不足。我在4K max-model-len配置下实测把max-num-seqs设为128时吞吐与延迟达到一个比较理想的状态。--block-sizePagedAttention的页大小默认是16。一般不需要改但如果你观察到KV Cache分配粒度过粗或过细可以尝试调整。V100上的默认16表现良好我没有额外改动。--enforce-eager关闭CUDA Graph会降低性能但减少显存占用。V100的显存足够没必要开启让CUDA Graph模式保持开启反而能提升推理效率。启动完成后最简单的测试方式curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: your-model, messages: [{role: user, content: 你好}], max_tokens: 64 }能看到正常返回文本说明服务已经起来了。3.3 首个推理请求验证服务启动后第一个要做的是验证单请求的延迟和显存情况。我习惯用一段固定中文输入、输出长度为256 token来测试curl http://localhost:8000/v1/completions \ -H Content-Type: application/json \ -d { model: your-model, prompt: 请简单介绍一下深度学习的发展历史。, max_tokens: 256, temperature: 0.7 }此时观察三个指标首token时间TTFT、生成速度tokens/s、显存占用。V100的生成速度相比A100确实弱一些7B模型单请求FP16大概能跑到40-80 tokens/s具体取决于序列长度和显存频率。如果单请求生成速度低于30 tokens/s就要检查是否TP通信存在瓶颈、是否模型量化格式不适配等问题。首token时间同样值得关注。V100在TP8模式下prefill阶段涉及多卡AllReduce如果卡间通信性能不佳TTFT会明显偏高。经验参考正常的7B模型、输入长度512时TTFT应该在300ms-800ms之间。如果超过1.5秒建议优先排查拓扑和驱动。4. 业务吞吐测试方案设计4.1 测试场景与请求特征模型部署好接下来就是压测。实际业务场景和单纯的最大token吞吐测试差别很大因为真实请求的输入输出长度并不均匀。有的请求只有一句简短问题输出可能很长有的请求输入一长串资料输出却很短还有的请求会携带系统提示词这部分也算在输入序列中。本次压测参考了业务方提供的历史日志提炼出三类典型请求短输入短输出输入200字符以内输出200 token以内占比约40%。中长输入中长输出输入800字符以内输出500 token以内占比约40%。长输入长输出输入2000字符以内输出1000 token以内占比约20%。压测时不能只用单一长度的请求而是按比例随机生成才能还原真实业务的混合特征。实际执行时我写了一个脚本从模板库中随机抽取问题文本并拼接不同的上下文长度。4.2 压测工具选型与实现压测工具有很多选择wrk、JMeter、Locust、vLLM自带的benchmark脚本等。wrk不支持HTTP body的复杂构造JMeter重且配置繁琐。我最终采用了Python ThreadPool aiohttp的方式自己写了一套并发请求工具。核心代码结构如下import asyncio import aiohttp import time import random import statistics URL http://localhost:8000/v1/completions MODEL your-model CONCURRENCY 50 TOTAL_REQUESTS 500 async def send_one(session, prompt, max_tokens): payload { model: MODEL, prompt: prompt, max_tokens: max_tokens, temperature: 0.7, stream: False } start time.time() async with session.post(URL, jsonpayload) as resp: data await resp.json() elapsed time.time() - start return elapsed, data.get(usage, {}) async def main(): async with aiohttp.ClientSession() as session: tasks [] for i in range(TOTAL_REQUESTS): prompt, max_tokens gen_request_by_distribution() tasks.append(send_one(session, prompt, max_tokens)) results await asyncio.gather(*tasks) # 统计这段脚本虽然简单但足够支撑本次压测需求。如果你希望更精细地控制请求到达速率可以改造成信号量限流的模式或直接调用vLLM项目里提供的benchmark_serving.py它支持并发数、请求总数、输入输出长度分布等参数统计也更规范。4.3 核心指标定义压测不能只盯着一个平均延迟。我们需要定义一套统一的指标口径否则后续对比和容量规划全都无从谈起。TTFTTime To First Token从请求发出到返回第一个token的时间反映用户的等待感。TPOTTime Per Output Token生成每个token的平均时间通常用总延迟 - TTFT/ 输出token数来估算。端到端延迟完整请求从发出到全部返回的时间。吞吐率Req/s单位时间完成的请求数。Token吞吐率Tokens/s单位时间生成的token总数包括输入和输出更能反映GPU实际算力利用率。并发数同时打向服务的请求数量。此处不是最大并发而是测试时实际维持的并发水位。这六个指标相互关联单独看都有局限。比如追求Token吞吐率最大可以盲目把并发拉高但此时TTFT可能会暴涨到用户无法接受反过来只压TTFT并发太低又浪费了GPU资源。所以测试时要同时记录这些指标以它们之间的变化关系来判断服务的最优工作点。5. 实测数据与结果分析5.1 低并发下的延迟画像先看低并发数据。我设置了5个并发和10个并发请求总数为200按4.1节的三类请求比例分布。初步结果显示低并发时所有请求几乎不会排队GPT的batch内部虽然有调度但整体压力较小。这里给出一组当时的实测参考数据模型为7B FP16TP8max-model-len4096max-num-seqs128并发数平均TTFTP95 TTFT平均端到端延迟平均生成速度总吞吐5385ms620ms4.2s55 tokens/s约1200 tokens/s10501ms840ms5.8s48 tokens/s约1800 tokens/s从数据可以看出低并发下TTFT表现不错P95也在1秒以内符合一般业务对首响应时延的预期。但端到端延迟并不乐观因为输出长度较长解码耗时占了大头。这也是生成式模型服务的基本画像并发低时延迟主要由模型解码速度决定与框架本身关系不大。5.2 并发提升后的吞吐变化与排队效应低并发测试是热身真正的压力测试从50并发开始。我将并发数从20逐步提高到80、120、160保持混合请求分布不变观察吞吐与延迟的博弈。并发数平均TTFTP95 TTFT平均端到端延迟Token吞吐率20680ms1.1s7.6s约2600 tokens/s501020ms2.3s12.4s约4200 tokens/s801890ms4.6s18.9s约5300 tokens/s1203120ms8.2s30.8s约6100 tokens/s1604820ms13.6s46.7s约6500 tokens/s从这组数据可以清楚看到vLLM的Continuous Batching特性并发从20涨到80时Token吞吐率翻了近一倍说明GPU算力没有被喂饱每个inference step处理的序列数量在增加算力利用率因此上升。但并发从120涨到160时吞吐增长明显放缓说明已经接近GPU算力上限此时新增的并发只是让请求排队等待并不能转化为有效吞吐。这里需要特别关注的是TTFT。当并发达到160时P95 TTFT已经超过13秒这在大多数在线场景下是不可接受的。对用户来说等了十几秒才看到第一个字跟服务挂了没什么区别。因此在确定服务容量水位时不能只看GPU是否满载还得结合业务可接受的TTFT指标。5.3 长文本场景下的显存优化长文本请求是压测中的大头也是显存杀手。我在测试中加入了最大输入2000字符、输出1000 token的场景发现并发较高时出现了部分请求超时甚至OOM的现象。分析过程是这样的每个输入token和已生成的输出token都会占用KV Cache。vLLM的KV Cache分配以block为单位默认block_size16每个token的KV Cache大小由模型层数、头数、维度决定。7B模型FP16下单个token的KV Cache大约在0.5MB-1MB水平。输出1000 token意味着一个长请求的KV Cache要消耗0.5GB-1GB。当并发中长请求比例较高时KV Cache消耗会迅速上升。解决思路有两个方向。第一压缩输入侧。上线前对业务侧的prompt做瘦身把系统提示词中无关的背景内容删掉长文本分段处理后再拼接。这能直接从源头减少KV Cache占用。第二调整vLLM的--max-model-len不要盲目设置过大的上限。比如业务实际最大输出只有1000 token、输入只有1500字符那么max-model-len设为2048比4096更合理显存利用更高效。另外值得一提的是如果想进一步压缩显存可以考虑KV Cache量化但V100对FP8支持有限INT8量化KV Cache会引入一定精度损失。本次业务对精度敏感所以最终没有启用仍然是FP16 KV Cache方案。6. 常见问题与排查实录6.1 显存OOM的排查思路我在压测中途遇到过几次OOM最开始的报错是CUDA out of memory。排查流程是这样的第一步先看nvidia-smi单卡显存占用。如果所有卡显存都满了说明KV Cache分配过大降低--gpu-memory-utilization或减小--max-num-seqs。第二步如果只是某几张卡显存爆了可能跟TP时的KV Cache分布不均有关此时要检查请求长度均衡性。第三步查看vLLM日志中的KV Cache pool占用率确认是碎片还是真实打满。实际中最常见的还是前两类。需要提醒的是--gpu-memory-utilization调的过低虽然能避免OOM但会浪费大量显存导致并发能力大幅下降。较稳妥的做法是先设0.9再根据压测数据的KV Cache使用率微调。6.2 请求排队时间过长或卡死高并发压测中请求排队时间上升是正常现象。但如果是大量请求长时间无响应就要检查--max-num-seqs是否设置过小导致任务在batch外排队。此时vLLM的日志里会频繁出现Waiting for available sequences字样的警告。另一个容易忽略的点是vLLM的默认请求超时设置。官方没有在启动参数里暴露超时时间所以如果某个请求因为生成过慢或客户端断连服务端仍会持续计算白白占用显存和算力。线上接入时建议网关层自己设置超时时间例如300秒并配合流式输出缩短用户等待感知。6.3 性能不达预期时的排查方向压测结果如果明显低于预期不要急着怀疑vLLM从几个方向逐一排查。GPU利用率是否达到90%以上如果GPU利用率很低说明瓶颈在CPU侧数据预处理、tokenization、调度开销可能过大。此时可考虑提高max-num-seqs、减少Python侧开销。卡间通信是否成为瓶颈TP8场景下每步decode都需要AllReduceV100 NVLink带宽有限。如果NVLink异常降级为PCIe模式性能会雪崩。用nvidia-smi topo -m或nvidia-smi nvlink -s确认NVLink状态。输出token数是否耗在请求本身的生成长度上如果大量请求输出长度很长即使吞吐不变每秒完成的请求数也会下降。所以对比时要统一请求特征不能只看单一指标。模型本身的损耗呢7B和13B的耗时差距不是线性的13B模型的attention计算量更大KV Cache占用也多综合吞吐会下降40%-60%。如果你的业务能拆分成多个小模型服务效果可能反而更好。6.4 业务超时兜底策略线上真实场景不可能等待30秒那么久所以必须在服务层做好兜底。我的做法是业务网关设置读超时为15秒超过则立刻返回预先写好的兜底回答同时启用vLLM的流式输出streamTrue这样首字TTFT可从原来的2-3秒降到几百毫秒用户感知上更即时。虽然流式输出不会降低总生成时间但配合前端打字机效果能显著降低用户的等待焦虑。此外还可以在网关层做并发限流。如果发现服务端P95 TTFT超过5秒主动拒绝一部分低优先级请求保证核心请求的服务质量。这套机制在高负载时可以避免服务进入拥堵崩溃的恶性循环。7. 调优建议与容量评估7.1 三个最值得做的调优点这一轮部署调试下来我认为在V100 32G上跑vLLM最值得投入精力的调优有三个。第一个是请求级调度参数调优。--max-num-seqs、--max-model-len这两个参数直接决定并发能力和显存分配需要结合真实业务请求的长度分布反复调。我最终选定max-model-len2048而非4096因为压测数据显示95%的请求不超过这个长度调低后KV Cache利用率提升了30%以上。第二个是开启前缀缓存--enable-prefix-caching功能。如果业务中有大量请求共享相同的前缀比如系统提示词、固定问答模板开启前缀缓存后这部分KV Cache可以直接复用既省算力又省显存。实测在固定前缀占比高的场景下TTFT降低约20%-30%。第三个是业务侧的Prompt瘦身。很多人只盯着推理框架忽略了业务prompt本身对性能的巨大影响。一个1000字符的系统提示词每次请求都要参与prefill计算。把无关的背景文字去掉只保留必需的指令与示例对吞吐提升的帮助比调任何框架参数都来得直接。7.2 容量评估与扩展建议最后聊聊容量评估。8张V100 32G到底能支撑多大的业务量这个问题没有标准答案但可以根据我们实测的数据做一个大致的推算。假设你的业务要求P95 TTFT小于5秒从测试数据反推这个服务大约能支撑80-100并发在混合请求分布下约能提供5000-6000 tokens/s的吞吐。如果每轮对话平均消耗300个token输入输出那么大约能支撑每分钟1000-1200轮对话。当然这只是一个粗略估算。实际业务中还需要考虑tokenize速度、模型大小、量化方式、请求长度分布等变量。更靠谱的方式是先用真实流量日志离线回放做一次较准确的压测再根据P95 TTFT和最大并发这两个硬指标来规划容量。扩展方向上如果业务量继续上涨不建议把TP从8改成16除非你有16张卡更务实的做法是增加一个TP8的副实例通过负载均衡把流量分散到多实例。这样故障域也变小一个实例挂了不至于全站不可用。这轮折腾下来我的总体感受是V100 32G跑大模型推理没有想象中那么不堪但也没有想象中那么轻松。它的瓶颈不在显存而在卡间通信带宽和单卡算力上限。vLLM的调度能力能把这几张老卡的潜力压榨出来但要达到业务可用的吞吐和延迟必须结合真实请求特征反复调参。如果你也是类似的存量硬件配置建议先做小流量压测验证再逐步放开线上流量切忌一上来就按最大并发配置来跑。