最近我把手头两张RTX 3090组了一台双卡推理机专门用来跑Qwen2.5-14B的BF16权重。折腾了几天踩了不少坑也把vLLM的分布式部署参数基本摸透了。这篇东西算是一个完整记录从硬件准备、环境安装、模型下载到双卡张量并行启动、KV Cache显存计算、并发压力测试最后附上常见报错速查表。如果你手里有两张3090或者两张24G显存的卡想本地私有化部署一个14B级别模型给团队内部用这篇文章可以直接照着抄。先说结论双卡3090跑Qwen2.5-14B BF16是一个非常甜点的配置。单卡24G显存跑14B BF16权重肯定爆显存上量化又会损失准确率而30系卡又吃不了FP8红利所以24G×2加BF16就成了性价比最高的临界点。vLLM通过张量并行把14B模型切成两半分别放到两张卡上推理速度反而比单卡跑量化模型更快更稳。下面我把整个部署过程掰开揉碎讲清楚。1. 方案选型为什么是双卡3090、Qwen2.5-14B和BF16很多人一上来就纠结“该买什么参数”其实选型本质是在算三笔账显存账、精度账、速度账。1.1 显存账为什么14B权重必须上双卡Qwen2.5-14B的BF16权重大约有28GB一张24G显存的3090无论如何放不下。量化到INT8大约是14GB单卡能跑但如果你的场景对输出质量有要求比如代码生成、结构化数据抽取、Agent推理INT8在某些任务上会明显弱于BF16。如果上32B模型BF16权重要到64GB左右24G×2也吃不下只能量化但14B是刚好能塞进双卡48G显存并且还有余量给KV Cache的档位。单卡3090适合跑7B/8B模型双卡3090适合跑14B/13B四卡则能摸到32B。这个梯度非常清晰硬上更大模型只会让KV Cache缩水、并发能力变差得不偿失。1.2 精度账BF16、FP16、FP8到底怎么回事先帮大家理清BF16、FP16、FP8这三兄弟。三者都是半精度浮点但设计思路完全不同。FP16就是IEEE标准的16位浮点数1位符号、5位指数、10位尾数。它的尾数精度高但指数范围窄导致数值一大就容易溢出比如计算注意力分数时如果值超过65504直接变Inf。当年很多人在老显卡上跑深度学习遇到loss变NaN十有八九就是FP16精度爆炸。BF16Brain Floating Point是Google搞出来的1位符号、8位指数、7位尾数。它的指数范围和FP32一样所以动态范围大很多不容易溢出代价是尾数精度差一点。但对神经网络来说尾数少几位影响不大权重和激活值的数值范围反而更重要。这就是为什么BF16在训练和推理中越来越主流。FP8是更新的东西只有8位分E4M3和E5M2两种格式。它能把显存用量再砍一半但问题是Ampere架构也就是RTX 30系在硬件层面没有针对FP8做优化真正能吃到FP8红利的是Ada架构RTX 40系和Hopper架构H100。所以你的3090跑FP8权重模型要么不支持要么速度并不理想不如老老实实用BF16。一句话总结30系显卡上选BF16是硬件能力范围内的最优解没有之一。1.3 速度账为什么推理框架选vLLM当前主流的本地推理方案有Ollama、vLLM、SGLang、text-generation-inference。Ollama适合单卡轻量使用胜在简单但它为了易用性牺牲了很多调度和控制能力。vLLM的核心优势有三个第一PagedAttention。vLLM把KV Cache像操作系统分页一样拆成小块按需分配显存利用率比传统预分配高得多这对双卡部署尤其重要因为显存本来就紧。第二Continuous Batching。传统框架一次只能处理一个batch遇到并发请求就排队vLLM是动态调度一个序列生成完一个token就立刻让出计算资源给其他序列。所以同样的硬件vLLM的吞吐量通常能翻好几倍。第三OpenAI兼容API。vLLM启动后直接提供一个/v1/chat/completions接口你现有的OpenAI SDK代码基本不用改填个base_url就能接上。这对团队内部工具链接入非常友好。SGLang在部分场景下性能更激进但vLLM生态更成熟、文档更全、坑更少。对双卡用户来说vLLM是最稳妥的选择。2. 硬件与系统环境准备避免性能瓶颈的几个关键点双卡3090部署大模型很多人只盯着显卡结果其他配件拖了后腿。我这次踩过分工明确的坑逐一说明。2.1 CPU与内存14B模型推理时CPU主要做预处理、tokenizer、调度真正重计算都在GPU上。但如果你开并发请求CPU会成为瓶颈。经验值是至少8核16线程最好12核以上。内存方面模型权重28GB加载时会经过CPU内存而且vLLM每个worker进程也会占一部分内存我建议32GB起步64GB比较舒服。我一开始用16GB内存加载模型时系统几乎卡死换成64GB后顺利很多。2.2 主板PCIe通道与NVLink双卡并行最核心的点是卡间通信。vLLM的张量并行会在每个Transformer层做AllReduce也就是同步两张卡上的中间结果。如果两张卡走的是PCIe 3.0 x16通信带宽大约是16GB/s如果插在PCIe 4.0 x16槽位上带宽翻倍到32GB/s如果上了NVLink3090的NVLink带宽可以达到约112GB/s单向约56GB/s。NVLink快非常多但不是所有3090都有NVLink金手指也不是所有主板都支持。实操建议通电前先看主板的PCIe插槽带宽如果能跑PCIe 4.0 x16就优先插这两个槽位。如果确实没有NVLink也不用太焦虑vLLM对PCIe通信做了不少优化14B模型在PCIe 3.0下用TP2速度会比NVLink慢一些但不会慢到不能用。我自己实测下来PCIe 4.0足够喂饱14B模型的大多数场景。2.3 电源与供电双3090满载功耗接近700W加上CPU、主板、风扇整机功耗随随便便破850W。我建议电源至少上1000W金牌最好是1200W。另外注意3090是出了名的“瞬时功耗爆炸”瞬间可能冲到350W以上所以电源不要卡着冗余买。供电接口方面每个3090一般需要一个3×8Pin或12Pin转接线提前确认电源线材够不够。2.4 系统与驱动vLLM目前对Linux支持最好Windows底下要WSL2性能损耗和麻烦事都多。我是用Ubuntu 22.04做的部署驱动用550系列或更新的版本CUDA用12.1以上。可以用nvidia-smi确认驱动状态用nvcc --version查看CUDA版本。如果驱动版本太老vLLM会直接报CUDA错误这时候不要慌去NVIDIA官网下载对应显卡的运行包重装驱动即可。注意如果你有多张显卡但只想让vLLM看到其中两张用环境变量CUDA_VISIBLE_DEVICES0,1来指定避免其他GPU进程干扰显存计算。3. vLLM环境搭建与模型下载这一节是关键实操环节我按实际操作顺序写。3.1 创建Python虚拟环境vLLM依赖PyTorch、transformers、tokenizers等一堆包强烈建议用虚拟环境隔离不要直接装到系统Python里。我用uv来管理Python环境和依赖速度比conda快一个量级curl -LsSf https://astral.sh/uv/install.sh | sh uv venv vllm-env --python 3.11 source vllm-env/bin/activatePython版本建议3.103.12之间太老的版本会导致某些依赖编译失败。3.2 安装vLLMuv pip install vllm安装完成后用python -c import vllm; print(vllm.__version__)验证。如果这一步报错先检查CUDA驱动和CUDA Toolkit版本是否匹配。vLLM默认下载的wheel是针对常见CUDA版本预编译好的正常情况不需要自己编译。经验不要图新鲜装每日构建版用稳定版就好。我在某次手滑装了dev版结果PagedAttention报了个莫名其妙的段错误回滚到稳定版就好了。3.3 下载Qwen2.5-14B模型权重有两种途径国内网络建议用ModelScope海外或服务器在国外用Hugging Face。ModelScope下载代码pip install modelscope modelscope download --model Qwen/Qwen2.5-14B --local_dir ./qwen2.5-14bHugging Face下载pip install huggingface_hub huggingface-cli download Qwen/Qwen2.5-14B --local-dir ./qwen2.5-14b下载完看一眼模型目录确认这几个关键文件都在config.json模型结构配置注意torch_dtype字段是bfloat16说明原点模型就是BF16权重model-00001-of-0000x.safetensors分片权重文件一共5~6个分片每个约5.5GBtokenizer.json和tokenizer_config.json分词器文件缺失会导致启动报错generation_config.json生成配置缺失会让默认生成参数不对我建议先下载到本地再用本地路径启动不推荐让vLLM每次从HF下载网络不稳定容易失败。3.4 双卡启动推理服务核心命令如下CUDA_VISIBLE_DEVICES0,1 vllm serve ./qwen2.5-14b \ --tensor-parallel-size 2 \ --dtype bfloat16 \ --gpu-memory-utilization 0.90 \ --max-model-len 32768 \ --max-num-seqs 16 \ --port 8000逐项解释--tensor-parallel-size 2最关键参数告诉vLLM把模型切到2张卡上并行推理。不设置这个参数vLLM会以为只有一张卡然后直接报显存不足。--dtype bfloat16显式指定加载精度。这里是配合BF16权重如果你下载的模型config里torch_dtype已经是bfloat16不写也行但建议显式指定避免默认走上FP16路径。--gpu-memory-utilization 0.90允许每张卡使用90%显存。剩下10%留给CUDA context和碎片化余量。如果经常OOM可以降到0.85。--max-model-len 32768最大上下文长度。Qwen2.5-14B本身支持128K但双卡显存有限设太大的max-model-len会让KV Cache预分配爆炸。--max-num-seqs 16最多同时处理的序列数。调低这个值能减少并发请求对显存的瞬间压力。--port 8000服务端口不冲突时可以不写。启动日志里如果看到类似After applying optimizations, change in kv_cache: 12.31GB的字样说明KV Cache空间已经分配好了。接着会看到Started server process再用curl http://localhost:8000/v1/models验证模型是否已注册。整个过程启动可能需要1~3分钟不等因为要加载28GB权重并完成张量并行初始化这是正常的别以为卡死了。4. KV Cache显存是怎么算出来的双卡部署和单卡部署最大的不同就是你得精确知道KV Cache能吃多少显存否则并发一多就OOM。这节我把账算透。4.1 一条常见但容易误判的规律机器学习圈有个流传很广的估算方法显存需求 ≈ 模型参数 × 2字节 激活值 KV Cache。在vLLM启动时显存其实被分成四块模型权重BF1614B×2≈28GBTP2时每卡约14GBCUDA上下文与运行时大约0.5~1.5GB/卡激活值与临时计算缓冲KV CachePagedAttention动态分配出来的部分所以当gpu_memory_utilization0.90时每卡大约21.6GB可用扣掉14GB权重和1GB运行时后每卡剩6~7GB给KV Cache和激活值。两卡加起来大约12~14GB的KV Cache空间。4.2 Qwen2.5-14B的KV Cache详细计算Qwen2.5-14B的结构参数是48层Transformer、hidden_size5120、40个注意力头、8个KV头GQA、每个头维度128。KV Cache每个token的字节数用这个公式算KV Cache字节/token 2K和V × 层数 × KV头数 × 头维度 × 每个元素字节数 2 × 48 × 8 × 128 × 2 196,608 字节 192KB/token这个数字非常惊人。换算一下如果你希望模型能处理32K上下文约2万汉字单条请求的KV Cache就要吃 32768 × 192KB ≈ 6GB。也就是说12~14GB的KV Cache空间同时只能容纳2~3条32K上下文的并发请求。如果要提升并发有两条路第一降低--max-model-len到16384或8192每条请求的KV Cache上限变小能容纳的并发序列变多。第二开启--kv-cache-dtype fp8_e5m2。vLLM在Ampere架构上也支持把KV Cache以FP8格式存储精度损失在可接受范围内但KV Cache字节直接减半从192KB/token降到96KB/token。我试过这个参数对14B模型来说效果明显输出质量没有肉眼可见的下降建议开启。4.3 跑一次实际计算的例子假设我设定--gpu-memory-utilization 0.90两卡总共KV Cache空间约13GB--max-model-len 32768--kv-cache-dtype fp8_e5m2那么单条请求在满上下文长度下需要约3GB KV Cache理论上13GB能支撑4个并发长上下文请求。如果实际业务里大部分请求只有4000 token左右约0.75GB并发能力就更宽松。vLLM的优越性恰恰在这PagedAttention不会给4096长度的请求预留32768的显存请求进来时按实际长度分配块。所以你的并发上限不是死的而是由真实流量分布决定的。这也是为什么vLLM比Ollama更适合做服务化的原因。5. 客户端调用与并发压力测试服务起来了接下来是怎么调、怎么压测、怎么看指标。5.1 OpenAI兼容API的调用方式vLLM启动后接口地址是http://localhost:8000/v1/chat/completions。用Python调用时直接替换OpenAI SDK的base_urlfrom openai import OpenAI client OpenAI( base_urlhttp://localhost:8000/v1, api_keyEMPTY, # vLLM默认不校验key但接口要占位 ) response client.chat.completions.create( modelqwen2.5-14b, # 模型名可以随意启动时没指定就用路径名 messages[ {role: system, content: 你是一个严谨的技术助手。}, {role: user, content: 用三句话解释什么是张量并行。}, ], temperature0.7, max_tokens512, ) print(response.choices[0].message.content)注意model字段在请求体里可以随便填吗不行虽然大多数情况下vLLM不校验但为了避免兼容性问题建议写成启动时传给vllm serve的模型名或路径最后一个目录名。5.2 用curl快速验证首token延迟命令行快速测试time curl -X POST http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2.5-14b, messages: [{role: user, content: 写一首关于秋天的短诗}], max_tokens: 128, stream: true }这里建议开启stream: true流式输出。流式模式下首token到来时间TTFT能直接感知出来不流式则需要等整个序列生成完才返回延迟体感差很多。双卡3090跑14B BF16TTFT通常在几百毫秒到1秒左右后端生成速度在30~60 token/s区间具体看输入长度和并发负载。5.3 并发压测与关键指标解读压测工具我直接用的是vLLM自带脚本python3 -m vllm.benchmarks.benchmark_serving \ --backend vllm \ --model ./qwen2.5-14b \ --dataset-name sharegpt \ --dataset-path ./ShareGPT_V3_unfiltered_cleaned_split.json \ --num-prompts 200 \ --request-rate 4 \ --max-concurrency 16关键看三类指标首token延迟TTFT发送请求到收到第一个token的耗时。这个指标对交互式体验最重要200ms和2s体感天差地别。单token延迟TPOT每生成一个token的平均耗时反映模型单步推理速度。吞吐率Throughput单位时间生成的总token数。并发场景下vLLM的continuous batching能大幅拉升这个数字。我实际跑下来的经验分布大概是单请求TTFT约0.8秒吞吐率能到1000 token/s多个并发请求叠加。相比单卡跑INT4量化模型双卡BF16的张量并行在延迟和准确率上都有优势。注意压力测试时留意GPU利用率。用nvidia-smi dmon -s pucvmet -d 1实时监控两张卡的利用率和显存变化。如果两张卡利用率不均衡比如一张99%一张70%很可能是张量并行切分不均匀或者通信瓶颈可以尝试换PCIe槽位或检查NVLink。6. 双卡部署常见问题与排查实录最后这部分专门讲坑。我在部署过程中踩过的、以及社区里高频出现的报错统一整理成速查表。6.1 启动阶段报错报错信息原因解决方案CUDA error: peer access is not supported两张卡之间无法P2P通信常见于未桥接NVLink或主板PCIe拓扑限制如果无法硬件解决在启动前设置NCCL_P2P_DISABLE1和NCCL_SHM_DISABLE1让NCCL走共享内存或CPU中转信道RuntimeError: Expected all tensors to be on the same device张量并行参数没设置成功或者驱动版本太老确认启动命令里有--tensor-parallel-size 2升级NVIDIA驱动到550CUDA out of memory显存分配不足降低--gpu-memory-utilization到0.85或降低--max-model-lenNo available memory for the cache blocksKV Cache空间被耗尽通常是因为max-model-len或并发序列太多调低--max-num-seqs缩短--max-model-len开启--kv-cache-dtype fp8_e5m2ModuleNotFoundError: vllm或_C扩展加载失败环境混乱或默认Python不对用which python确认当前在虚拟环境里重新uv pip install vllm不要混用conda和uv启动很慢卡在Loading model weights28GB权重加载加CUDA初始化正常现象耐心等待如果超10分钟没动静检查模型目录文件是否完整6.2 运行阶段常见性能问题运行一段时间后可能会发现吞吐量不如预期这里给几个自查方向检查两张卡的功耗。nvidia-smi里如果一张卡Power Usage只有80W另一张280W说明负载不均衡。常见原因是供电不足或PCIe链路降速。用nvidia-smi -q -d PERFORMANCE查看当前PCIe速率是x16还是x8。检查请求排队是否过长。vLLM的--max-num-seqs如果设得太小比如4并发请求会被强制排队TTFT会飙升。建议16~32起步跑压测找到最优值。如果开启了--enforce-eagerCUDA graph会被禁用吞吐量会下降明显。只有OOM时才考虑用这个参数。6.3 环境与部署的其他坑Windows原生跑vLLM别折腾了官方支持聚焦Linux。Windows上要么用WSL2要么直接换Ubuntu。多卡机器上有其他GPU任务用CUDA_VISIBLE_DEVICES0,1做隔离别让vLLM和别的进程抢显存。下载模型时网络中断导致权重损坏看config.json里model_index或分片文件大小是否和Hugging Face/ModelScope页面对得上不对就删掉重下。vLLM日志里警告The model is larger than 14GB这只是提醒模型超过了某些平台的自动量化友好阈值不是错误不用管。6.4 一个容易被忽略但非常致命的问题部署完成后客户端调用时如果发现模型总是输出空内容或者服务直接挂掉先去看日志里有没有Input validation error或ValueError:The models max model len is ...。这往往是请求里的max_tokens加上输入长度超过了--max-model-len。比如你设了--max-model-len 32768但请求上下文有30000 token你要求生成5000 token加起来超过32768vLLM会直接拒绝。所以制作应用时前端要做截断或滑动窗口策略把总长度控制在模型上限之内。我个人在实际操作中的体会是vLLM这套东西本身不复杂复杂的是显存规划和参数调优。只要把--tensor-parallel-size、--gpu-memory-utilization、--max-model-len这三件套配合好双卡3090跑Qwen2.5-14B BF16就能稳定运行很久。最后再分享一个小技巧启动时可以在命令里加一个--served-model-name qwen14b这样客户端调用时的model字段固定为qwen14b后续就算换底模客户端代码也不用改运维省心很多。这个环境后续接RAG做知识库、接Agent做工具调用都挺顺手属于那种一旦跑通就一直用下去的部署组合。