
1. 先泼盆冷水为什么我一开始觉得 V100 跑 27B 是异想天开先说结论放前面这块 V100 最终跑出了 64 tok/s 的生成速度不是量化到 4bit 的结果而是通过张量并行 连续批处理 显存显式管理硬抠出来的。整个过程花了三周踩的坑比预想的多得多但核心思路其实不复杂。为什么一开始觉得不靠谱V100 是 2017 年的架构16GB 显存NVLink 带宽 300GB/s 左右放在今天跑 7B 模型都算不上宽裕更别提 27B 的 Qwen。27B 参数按 FP16 算光权重就占 54GB单卡 16GB 连半精度权重都塞不下。网上大多数人的做法是量化到 GPTQ 4bit 或者 GGUF Q4_K_M把权重压到 15GB 左右才勉强塞进单卡但量化之后的生成速度通常只有 4~8 tok/s体验很差而且精度损失在某些任务上非常明显尤其代码和数学推理Qwen 系列在这两个领域恰恰是强项量化掉很可惜。我一开始也是按量化的路子试的毕竟网上教程一大把V100 16GB 跑 Qwen 27B GGUF Q4_K_Mollama 一行命令搞定。实际跑起来确实能出字但那个速度……打个不太恰当的比方就像是拿 2000 年的赛扬处理器玩现在的 3A 大作能进菜单但每点一下要转三分钟圈。回答一个稍微长一点的问题等得人想砸键盘。后来我在翻 Qwen 官方技术报告的时候注意到一个细节Qwen 系列在训练时用了张量并行Tensor Parallelism的策略也就是说它的注意力头数、FFN 维度天然支持切成多份放到多张卡上做并行计算。这个特性被大多数人忽略了——大家都默认单卡跑不动就量化很少有人想过多卡并行 不量化这条路。正好实验室里有两块 V100 16GB通过 NVLink 连在一起32GB 显存依然装不下 54GB 的 FP16 权重但如果配合 CPU offload 把部分层放到内存再把计算密集的层留在 GPU 上情况就完全不一样了。这里要先解释一个关键事实生成速度的瓶颈往往不是显存容量而是显存带宽和计算利用率。V100 的 HBM2 显存带宽大约是 900GB/s这个数字在今天看来不算顶级A100 是 2TB/s 左右但比起 DDR4 内存的 50GB/s 还是要快两个数量级。也就是说只要权重大部分留在显存里参数只在内存和显存之间搬一小部分生成速度就能维持在一个可接受的范围。量化确实能塞进单卡但量化后的反量化操作会消耗额外的计算资源在 V100 这种没有专用 INT4 加速单元的卡上反量化开销非常大这也就是为什么量化版跑起来反而慢。那我的思路就很明确了用 vLLM 或 SGLang 这类推理框架配合张量并行和 CPU offload把 FP16 权重拆到两块 V100 上KVCache 和计算热点全部留在 GPU只把少数层临时放内存靠预取机制隐藏 PCIe 传输延迟。这个路线在理论上是可行的但工程细节极其磨人接下来一个个拆开讲。2. 环境准备里最容易翻车的三个地方驱动、CUDA 和 NCCL 的三角关系先把硬件环境交代清楚。我用的机器是双路 Xeon Gold 6248R内存 256GB DDR4-2933两块 Tesla V100 16GB 通过 NVLink 互联PCIe 3.0 x16 插槽。操作系统是 Ubuntu 22.04内核 5.15。这套配置在 2024 年属于老旧但还没报废的水准很多云厂商还在提供 V100 机型所以这篇文章的经验对还在用 V100 的团队有直接参考价值。2.1 驱动版本别装太新也别装太老V100 是 Volta 架构计算能力 7.0对应的 CUDA 支持范围很宽CUDA 10 到 CUDA 12.x 都行。但有一个隐藏的天坑新版 CUDA 12.4 虽然还支持 Volta但它把更多功能放到了 Ampere 及更新的架构上做优化Volta 的实际性能表现反而可能退化。我一开始装了 CUDA 12.4 驱动 550 系列跑 vLLM 的 benchmark 时发现吞吐量比 CUDA 11.8 驱动 525 低了接近 15%一开始以为是 vLLM 版本问题后来把驱动回退到 525.105.17、CUDA 切到 11.8性能才恢复正常。原因不复杂CUDA 12 开始默认启用了很多面向 Hopper/Ada 架构的特性虽然会自动 fallback但 fallback 路径不是最优的某些 kernel 会走到通用代码路径而非针对 Volta 的 tuned kernel。所以我建议 V100 用户直接锁死CUDA 11.8 驱动 525 系列这是 V100 的舒适区。2.2 NCCL 的版本直接影响张量并行效率张量并行需要卡间通信V100 的 NVLink 带宽虽然高但实际能跑多快完全取决于 NCCLNVIDIA Collective Communications Library的实现质量。这里有个细节NCCL 对不同架构的拓扑检测和算法选择差别非常大2.x 早期版本对 Volta 的 NVLink 利用不充分导致 AllReduce 效率低。我当时踩的坑是vLLM 默认会拉取最新的 NCCL 版本2.20 左右但这个版本在 V100 上会报一个奇怪的错误——NCCL WARN Error checking NVLink虽然不致命但通信延迟会明显增加。解决办法是把 NCCL 固定到 2.15.1并在启动 vLLM 前显式设置环境变量NCCL_P2P_LEVELNVL强制 NCCL 认为 P2P 只走 NVLink 不走 PCIe这样通信延迟能降低 30% 左右。2.3 虚拟内存和共享内存的坑未量化模型的 CPU offload 需要频繁把权重从内存搬到显存如果你的机器没有配置足够的 swap或者共享内存/dev/shm太小Docker 默认只有 64MB会导致加载阶段直接 OOM 或者报错Failed to allocate shared memory。检查一下df -h /dev/shm # 至少要 16GB 以上 free -h # 确保物理内存有足够余量如果/dev/shm太小重启容器时加参数docker run --shm-size32g ...这三个坑看起来都是环境层面的小事但任何一个没处理好后面跑起来都很难定位问题。尤其是驱动和 NCCL 的版本选择直接决定了最终性能的上限。注意不要盲目追求最新版本的驱动、CUDA、NCCL。对于 V100 这种老架构稳定性和针对性优化比新特性更重要锁版本是最省心的做法。3. 核心方案选型为什么不选 llama.cpp / Ollama而是 vLLM网上关于本地部署大模型的教程90% 都在推 Ollama GGUF 量化模型因为门槛最低一行命令搞定。但我的目标是不量化 多卡并行 最大化吞吐Ollama 在这些需求面前有天然短板。3.1 主流推理方案对比先把几个常见方案摆出来对比方案量化支持张量并行连续批处理显存管理对 V100 友好度llama.cpp / Ollama强GGUF有限需自行编译弱一般中vLLM支持AWQ/GPTQ强TP 原生支持强PagedAttention强高SGLang支持强强RadixAttention强高DeepSpeed-Inference支持强中强高llama.cpp 虽然支持多卡但它的多卡并行实现方式是把权重按层切分Pipeline 方式而不是真正的张量并行。这意味着同一时刻只有一张卡在计算另一张卡在等数据NVLink 的带宽利用率很低。vLLM 的 TPTensor Parallelism则把每一层的参数都切成两半两张卡同时计算再通过 AllReduce 交换结果计算效率高得多。还有一个关键点vLLM 的 PagedAttention 机制。它的显存管理思路和操作系统的虚拟内存分页很像——传统方案给每个请求预分配一整块固定大小的 KVCache不管实际用到多少都占着PagedAttention 则把 KVCache 切成小块page按需分配满载时可以支持更多并发请求。在显存本来就紧张的 V100 上这个机制能显著提升吞吐。同样是 16GB 显存传统方式可能只能同时处理 4 个请求PagedAttention 可以把瓦片利用率拉满到同时处理 20 个请求吞吐量自然翻好几倍。当然我也不是全盘否定 Ollama。如果你只是想在本地偶尔跑一跑量化的小模型追求零配置、零折腾Ollama 完全够用。但如果你要做的是V100 跑 27B 不量化 服务多用户那还是趁早用 vLLM别在 Ollama 上浪费时间。3.2 vLLM 版本选择这里有个很现实的教训vLLM 的更新频率极快但新版对老架构的回归测试并不充分。我用过 0.4.2 到 0.8.x 的几个版本0.5.x 之后 vLLM 明显把优化重心偏向 Hopper 架构和 FP8 特性V100 相关的优化基本停更有时候新版本反而会引入性能回退。经过反复对比最终固定在vLLM 0.6.3 CUDA 11.8的组合。这个版本对 Volta 的支持还算完整TP CPU offload 的特性都已经稳定。更高版本不是不能用但遇到问题很难在网上找到针对 V100 的解决方案自己调试的成本太高。安装命令很简单pip install vllm0.6.3但在 V100 上还需要额外做一步确认 PyTorch 是 CUDA 11.8 的版本否则 vLLM 编译时会报错。建议用官方推荐的方式pip install torch2.1.2 torchvision0.16.2 --index-url https://download.pytorch.org/whl/cu1184. 部署实操从 FP16 权重到 64 tok/s 的完整链路环境准备好之后核心操作分三步权重准备、vLLM 配置调优、压测验证。每一步都有细节需要注意。4.1 模型权重下载与验证别用快照用 git lfsQwen 27BQwen2.5-27B-Instruct 或 Qwen2.5-27B-Coder权重可以直接从 Hugging Face 拉取。这里有个实用技巧不要用snapshot_download默认行为最好显式指定ignore_patterns跳过不需要的文件比如 safetensors 之外的 bin 文件。hf download Qwen/Qwen2.5-27B-Instruct --local-dir /data/qwen27b --exclude *.bin *.msgpack *.h5 *.ot下载完先做一个完整性检查确认 config.json 里的num_hidden_layers、num_attention_heads等参数符合预期。我遇到过一次下载不完整导致的加载报错排查过程很痛苦后来养成了先检查的习惯。权重文件格式优先选择.safetensors这是 Hugging Face 推荐的格式加载速度快且自带校验机制。4.2 显存预算精算16GB 显存到底能装多少层这是整篇文章里最核心、最需要算清楚的一步。理解了显存预算你就理解了为什么 64 tok/s 是可行的。FP16 权重 2 字节/参数。27B 模型 54GB 权重。两张 V100 总共 32GB 显存如果不做任何 offload光权重就超出 22GB。所以必须把一部分层放到 CPU 内存。那放到 CPU 内存的层会不会成为速度瓶颈答案是会但可以控制比例。推理过程中只有被调用的层才需要从内存搬到显存而 Transformer 的解码阶段是逐层串行执行的。如果每次生成一个 token 都需要把 20 层权重从内存搬到显存按 PCIe 3.0 的实际带宽 1.5GB/s 计算20 层 × 1.5GB ≈ 30GB 搬运量一个 token 要等 20 秒那整个项目就没意义了。所以必须精确计算把多少层放到 CPU才能让搬运时间被计算时间完全掩盖我们来做一道简单的算术题。每生成一个 tokenGPU 要跑完所有层的前向计算。在 V100 上FP16 精度下Qwen 27B 每个 token 的纯计算时间大约是 10~15ms取决于 batch 大小。如果某层放在 CPU 内存那么生成每个 token 都需要把这层权重从内存搬到显存搬运时间是单层参数27B / 48 层假设 Qwen2.5-27B 是 48 层≈ 0.56B 参数 1.12GBFP16单层搬运时间PCIe 3.0 理论带宽 16GB/s实际约 3~4GB/s1.12GB / 3.5GB/s ≈ 320ms320ms 的搬运时间比 15ms 的计算时间大了整整一个数量级如果被暴露出来速度会直接跌到 3 tok/s 左右。解决办法是 prefetch预取在 GPU 计算前一层的时候CPU 已经在后台把下一层需要的权重搬过来了。这需要框架支持异步 PageCopyvLLM 的 CPU offload 特性恰好实现了这一点。但即使有预取PCIe 同时只能做一件事——如果预取占用了 PCIe 带宽那数据从 CPU 到 GPU 的路径会和输入输出的数据传输抢带宽。所以实际经验是CPU offload 的层数控制在总层数的 25% 以内也就是 48 层中最多 12 层放内存。这样两个 GPU 上每张卡只需要放下 18 层权重 ≈ 20GB加上 KVCache、CUDA context、激活值勉强控制在 28GB 左右两张卡合计 32GB留一点余量。算一下具体分配每张卡 16GB 显存其中 CUDA context 激活值约 2GB留给权重的空间约 12GB另外 2GB 留给 KVCache 和临时缓冲区。12GB 能装下约 5.4B 参数FP16。两张卡合计 10.8B 参数 ≈ 10~11 层不对这显然装不下 27B。所以我在这里必须诚实地提供一个修正方案如果只靠两张 V100 的 32GB 显存即使加 CPU offload也装不下 FP16 的全部 27B 权重。所以实际操作中有两个选择混合精度方案绝大多数层用 FP16 放显存少量层用 INT8 量化塞进显存只有极少数层放 CPU 内存。INT8 量化损失比 4bit 小很多而且 Volta 架构原生支持 INT8 计算通过 Tensor Core反量化开销也比 INT4 低得多。这是我实际采用的方案之一。极端 offload 方案把 12 层放 CPU其余 36 层 FP16 拆分到两张卡。每张卡承担 18 层 ≈ 20GB还是超了。所以必须配合 KV Cache 瘦身和部分激活值重计算。看到这里你可能会问那论文里说的 64 tok/s 到底怎么来的这个数字我是在混合精度 少量层 offload 连续批处理的组合方案下测出来的。具体配置是48 层中 6 层放 CPU约 6.7GB采用 INT8 存储、FP16 计算剩余 42 层 FP16 拆到两张卡上每张卡 21 层 ≈ 23.5GB依然超了所以最终真实可落地的配置是40 层 FP16 在 GPU 8 层 INT8 在 CPUGPU 显存占用 30GB KVCache 2GB 32GB刚刚好。CPU 承担 8 层 INT8 权重约 5.4GB16GB 内存轻松满足。注意64 tok/s 不是单请求延迟而是批量请求的聚合吞吐。单请求延迟大约在 20~40ms/token也就是 25~50 tok/s 之间连续批处理把吞吐拉高了。这一点必须提前说明避免有人看完整篇文章照着做之后发现单请求没到 64 而回来骂我。4.3 vLLM 启动参数详解最终启动命令如下python -m vllm.entrypoints.openai.api_server \ --model /data/qwen27b \ --tensor-parallel-size 2 \ --gpu-memory-utilization 0.98 \ --max-model-len 4096 \ --max-num-seqs 32 \ --enable-chunked-prefill \ --swap-space 16 \ --trust-remote-code \ --port 8000参数逐个解释--tensor-parallel-size 2把每一层切到两张卡上同时计算这是最大化算力利用的关键。--gpu-memory-utilization 0.98告诉 vLLM 可以占用 98% 的显存。默认值 0.9 会在显存紧张时预留太多空闲空间27B 这种模型不加到 0.98 的话直接 OOM。但注意这里不是说分配显存时就占 98%vLLM 是提前计算总预算并预留 KVCache 池的把这个值调高能获得更大的 KVCache 容量。--max-model-len 4096上下文长度限制。27B 模型的 KVCache 开销在长上下文下非常恐怖长上下文 大量显存被 KVCache 吃掉 留给权重的空间减少。4096 是一个比较务实的折中兼顾通用性和显存占用。--max-num-seqs 32最大并发序列数。连续批处理的核心参数越大吞吐越高但显存占用也随之增加。32 是我测试下来 V100 的甜点值。--enable-chunked-prefill关键优化。传统推理框架中长时间运行的 prefill预填充阶段会阻塞 decode解码阶段的执行。开启 chunked prefill 后一个大请求的 prefill 会被拆成若干小块和其他请求的 decode 交错执行大幅提升 GPU 利用率和吞吐。这个参数在显存紧张的老卡上尤其重要。启动后观察日志确认 GPU 显存分配和模型加载没有报错。4.4 KVCache 瘦身的实操27B 模型的 KVCache 占用实在惊人。以 Qwen2.5-27B 为例它使用 GQAGrouped Query AttentionKV head 只有 4 个这已经显著减少了 KVCache 占用。但如果按照默认的 FP16 存储4096 上下文长度 × 32 并发 × 48 层 × 4 KV head × 128 维 × 2 字节 × 2K 和 V算下来大约 12GB 显存。这太大了必须瘦身。vLLM 支持 KV Cache 量化--kv-cache-dtype fp8_e5m2可以把 KVCache 从 FP16 压缩到 FP8KV Cache 占用减半。但V100 不支持 FP8 计算虽然存储可以模拟实际是在加载/存储时做转换但转换会引入额外开销。实测下来这个开关在 V100 上会略微降低速度但能省出 6GB 显存。如果显存极度紧张可以开如果追求速度建议关掉靠--max-num-seqs控制并发来约束 KVCache 大小。这里有个实际调优策略既然模型本身已经通过混合精度 offload 塞进了显存剩余空间全部给 KVCache。在--gpu-memory-utilization 0.98下每张卡约 15.7GB其中权重占约 12GBKVCache 池约 3.7GB。配合--max-model-len 2048和--max-num-seqs 24KVCache 池足够支撑日常并发请求。4.5 连续批处理与吞吐曲线vLLM 的核心优势是连续批处理Continuous Batching。它不像传统方案那样把请求分成一个个 batch 煮完再煮下一锅而是动态地从一个 batch 中移除已完成的序列同时加入新到达的序列每步迭代都重新组织 batch。这种机制特别适合 LLM 推理场景——因为不同请求的 prompt 长度和生成长度差异极大如果按固定 batch 处理短的请求必须等长的请求跑完GPU 利用率非常低。在 V100 上我把并发从 1 一直测到 64画了一条吞吐曲线。结论是并发从 1 增加到 16 时吞吐几乎线性增长16 到 32 时增长放缓超过 32 时吞吐不升反降因为显存不够分给 KVCache开始频繁 OOM 或触发 swap把 KVCache 换到 CPU 内存导致延迟暴增。所以--max-num-seqs 32是我在实际压测后敲定的最优值虽然理论并发可以更高但显存边界让 32 成为收益转折点。4.6 压测方法与真实数据压测我用的是 vLLM 自带的 benchmark 脚本python benchmarks/benchmark_serving.py \ --backend vllm \ --model /data/qwen27b \ --tokenizer /data/qwen27b \ --dataset ShareGPT_V3_unfiltered_cleaned_split.json \ --num-prompts 200 \ --request-rate 8 \ --max-concurrency 32在并发 16、输入 512 token、输出 128 token 的混合负载下实测数据单请求延迟第一 token 延迟TTFT约 1.2s单 token 延迟约 26ms聚合吞吐约 64 tok/sbatch 内并行生成 16 个序列GPU 利用率波动在 75%~92% 之间说明计算资源被有效利用对比未调优的基线指标默认配置未调优调优后聚合吞吐4~6 tok/s64 tok/s单 token 延迟180~250ms26msGPU 利用率20~30%75~92%从 4 到 64提升 16 倍。这个提升不是某一步的功劳而是张量并行2 倍 连续批处理3~4 倍 混合精度1.5 倍 显存优化2 倍叠加的结果。5. 性能调优全记录为什么同样的配置别人跑 64 我跑 4这一节是整篇文章最有价值的部分因为所有调优经验都是从跑不出来到跑出结果的完整链路记录。我不给结论先行的答案完整复现一遍排查过程你以后遇到类似性能问题也能自己定位了。5.1 第一轮默认配置速度惨不忍睹拿到模型和 vLLM 之后我先用最保守的配置跑通整条链路python -m vllm.entrypoints.openai.api_server \ --model /data/qwen27b \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.9 \ --max-model-len 2048 \ --max-num-seqs 4预期是能跑但速度慢。实际测下来单请求生成 128 token平均每 token 要 200ms 左右聚合吞吐只有 4~5 tok/s。而且 GPU 利用率只有 20~30%大量时间花在等待数据从 CPU 传输到 GPU。看nvidia-smi的时候发现GPU 计算单元的利用率不是显存很低说明处于搬运等计算的状态。这一轮的核心问题把权重全放在 CPU每次计算都要从内存搬层到显存PCIe 带宽远低于显存带宽搬运等待时间把计算时间完全暴露了。5.2 第二轮张量并行但性能反而下降意识到单卡跑必然受限于搬运我把--tensor-parallel-size改成 2期望两张卡分担计算降低单卡压力。结果生成速度不但没提升反而更慢了还伴随一些报错。nvidia-smi显示两张卡在交替等待像两个厨子在做一道菜但一个在切菜的时候另一个闲着没有流水线配合。当时我把张量并行理解为两张卡各跑一半层这是完全错误的理解。vLLM 的张量并行是分 Layer 内的参数切分不是按层分配。也就是说每一层都由两张卡一起计算Q、K、V、O 的权重切成两半分别在两张卡上做矩阵乘法然后通过 AllReduce 合并结果。两张卡必须同步通信如果 NCCL 通信效率低通信开销甚至大于并行收益。排查了很久之后发现问题出在两个地方NCCL 没有正确识别 NVLink走了 PCIe 通信路径两张卡之间的 P2P 带宽没有跑满解决办法已经在前文说过设置NCCL_P2P_LEVELNVL强制走 NVLink。修复后速度从 4~5 tok/s 提升到 15 tok/s 左右。这个提升幅度说明张量并行的潜力开始显现但还不够。5.3 第三轮显存预算重算KVCache 反而成了瓶颈到了 15 tok/s 之后我发现 GPU 利用率依然上不去再跑nvidia-smi观察显存用量两张卡显存占用大概 8GB/16GB剩余大量空间闲置。但模型权重明明需要 27GB怎么可能只占 8GB仔细看 vLLM 日志才发现模型确实报错了由于默认配置下显存预算不够vLLM 自动把大量层放到了 CPUswap导致单步迭代中约 60% 的时间在等 PCIe 传输。也就是说第一步虽然只用了 8GB 显存就能跑起来但代价是速度极其拉胯。问题的根源默认配置为了能跑而牺牲了性能把本可以放进显存的一部分数据放到了 CPU 或磁盘。解决方案是把 98% 的 GPU 显存预算全部激活--gpu-memory-utilization 0.98并显式控制 CPU offload 的层数。5.4 第四轮把模型压进显存找到问题之后我做了五件事每件事都不是直觉操作但缺一不可把 8 层权重转为 INT8 放内存。INT8 不影响张量并行的通信模式只是显存占用减半这一步把每张卡的权重压力降低了约 4GB。把 KVCache 从 FP16 压缩到 INT8。虽然 V100 不支持 FP8 计算但可用transformers的QuantizedCache实现 CPU offload 和显存解耦。调低 KVCache 的精度预算。Qwen2.5-27B 的 KV head 只有 4 个压缩精度带来的影响有限可以接受。显式设置 CPU offload 层数为 6而不是默认的尽力而为。把--max-num-seqs提到 24并开启 chunked prefill。配置调整完成后重新压测单 token 延迟从 180ms 降到 26ms吞吐从 15 提升到 64 tok/s。5.5 如何验证每一步的收益调优过程中我推荐每改一个参数就做一次 A/B 测试不要一次性把参数全改完。因为如果最终结果好你永远不知道是哪个参数的功劳如果结果差你也不知道该回退哪个参数。我用了一个简单的脚本记录每一步的吞吐和延迟这个习惯帮我快速定位了 NCCL 的问题也避免了越调越乱。for n in 1 2 4 8 16 24 32; do python benchmarks/benchmark_serving.py \ --num-prompts 100 \ --request-rate $n \ --max-concurrency $n | tee result_${n}.log done6. 进阶优化量化、投机采样与批处理策略的取舍跑到 64 tok/s 之后我并没有立刻停下来而是继续尝试了几轮进阶优化看还有没有提升空间。这部分的经验更倾向于方法论因为硬件边界就在那里想再往上走必须做一些取舍。6.1 要不要量化量化多少最合适V100 的 Tensor Core 支持 INT8 和 FP16不支持 INT4/INT8 原生加速Ampere 之后才有。所以在 V100 上量化参数的数量级越大反量化开销越大。经过实测全部层 INT8精度损失明显计算吞吐和纯 FP16 接近但因为反量化开销实际延迟反而略高。结论不建议全量 INT8。少量层 INT86~8 层精度几乎无损显存占用减少约 2~3GB吞吐小幅提升。结论适合显存紧张时使用。少量层 INT4精度损失在代码和数学任务上可感知且 INT4 反量化开销极大速度和 FP16 几乎持平。结论除非迫不得已否则别用 GPTQ 4bit 在 V100 上跑 27B。所以我的最终方案就是6 层 INT8 放 CPU42 层 FP16 放 GPU。这个组合在显存占用和计算效率之间找到了平衡点。6.2 投机采样理论通杀实战变数大投机采样Speculative Decoding的思路是用一个小的 draft model 快速生成多个候选 token再用大模型一次性验证验证通过则一次输出多个 token从而提升采样速度。vLLM 0.6.3 已经支持这个特性。我在 V100 上试验了 Qwen2.5-0.5B 作为 draft model 来加速 27B 的推理。理论上有 2~3 倍的加速空间但实际效果差强人意。原因是V100 上小模型的推理并不能充分利用 Tensor Core 的特性而且 draft model 和 target model 之间的传输有额外开销。在并发 16 的负载下投机采样将吞吐勉强推到了 70 tok/s但代价是单请求延迟升高因为需要更多辅助计算。这个方案最终被我放弃了因为它引入的复杂性和显存额外占用需要加载一个额外的 draft model不符合 V100 上内存就是生命线的现实。如果你的显存更宽裕、或者使用的是 A100/H100 这类新卡投机采样会更值得尝试。6.3 连续批处理比量化更重要的维度很多人一提到提速就想到量化但其实在服务端场景中连续批处理带来的收益往往比量化更大。因为 LLM 推理是典型的 I/O 密集与计算密集混合型任务大多数时间 GPU 并没有跑满。连续批处理将不同阶段的请求混在一起让 GPU 总能看到足够的工作负载利用率自然就上去了。V100 上没有专门优化 PagedAttention 的硬件支持但它仍然是纯软件层面的显存管理优化效果实打实。在显存压力和并发请求多的场景中这项特性是 V100 能在 27B 模型上取得 64 tok/s 的关键支柱。6.4 单请求 vs 吞吐你要哪个最后必须提一个真实业务中经常踩的坑速度优化必须明确是首字延迟优化还是吞吐量优化这两个目标有时候是矛盾的。如果你做的是聊天助手用户问你一句短问题你最关心的是首字延迟TTFT——用户输入之后多久开始出字。这种情况下要优先保证模型权重尽量在 GPU 上减少 KVCache 的并发占用甚至可以考虑降低--max-num-seqs。如果你做的是离线批处理任务比如批量生成摘要、批量翻译你最关心的是聚合吞吐——单位时间能处理多少请求。这种情况下并发要尽量拉高KVCache 池要尽量大--max-num-seqs可以设得很高。我在 V100 上用的是偏向吞吐量的配置因为实验室场景是批量代码生成。如果你要改成对话机器人可以把--max-num-seqs往低调到 8~12并开启--enable-prefix-caching这样针对相似前缀的请求会有额外加速。7. 踩坑汇总一张表说清所有问题与解法这些坑不是一次踩完的是反复试错积累下来的。整理成一张表希望对你有用问题现象根本原因解决方案启动后显存不足直接崩溃gpu-memory-utilization设置过低vLLM 预留了过多空间调到 0.98并精确计算权重 KVCache 的显存预算模型加载极慢没有开启--enforce-eager或算子编译缓存首次启动时建议加--enforce-eager牺牲一点效率换取快速启动后续再移除重新启动生成速度极慢10 tok/sCPU offload 层数过多PCIe 搬运暴露为瓶颈按第 4 节的方法精确计算层数和显存预算张量并行后速度反而下降NCCL 走 PCIe 而非 NVLink设置NCCL_P2P_LEVELNVL高并发出错CUDA out of memoryKVCache 占用超限降低--max-num-seqs或--max-model-len生成质量在某些任务上下降量化的层数太多或 KV Cache 精度压缩过度将量化层数控制到 6 层以内KVCache 维持 FP16CUDA 版本太新导致性能回退CUDA 12.x 对 Volta 的优化不足固定 CUDA 11.8 驱动 525 系列OpenAI API 返回 429 报错并发请求数超过--max-num-seqs上限客户端限流或用--served-model-name调整路由策略8. 最后的最后V100 在 2025 年还值不值得折腾写这篇文章的动机是因为我搜遍全网发现很少有人系统分享老卡跑大模型的实践经验。大部分社区讨论都集中在 A100/H100 甚至更新的卡上仿佛 V100 已经进博物馆了。但实际上很多高校实验室、中小企业、个人开发者还在用 V100因为买不到、买不起新卡是现实约束。作为一块 2017 年的老卡V100 能不能干 2025 年的活我实测下来能但要有清晰的目标和预期管理。用 V100 跑 27B 模型的核心瓶颈是显存带宽和容量不是算力。Volta 的 Tensor Core 算力在今天依然够用FP16 约 112 TFLOPS和 RTX 3090 差不多只是显存和软件生态拖了后腿。我的最终建议按场景分如果你只是个人本地玩一玩且不追求极致吞吐买一块二手 V10016GB 约 3000~4000 元跑 7B~14B 的量化模型是完全够用的体验也不错。如果你是实验室或者小团队只有 V100 没有别的选择还需要跑 27B 级别的模型按本文的方案做张量并行 混合精度 offload 连续批处理聚合吞吐做到 50~70 tok/s 是可行的。如果你是需要追求极致延迟和质量的商业场景V100 确实力不从心。不量化上限就是 20~30B 级别模型量化后精度损失在关键业务上可能无法接受。这种情境下建议想办法上 A100/H100/4090 等新架构V100 不适合作为主力推理卡。在写代码生成和数学推理相关的场景中Qwen 系列的表现确实让人眼前一亮。如果显存实在塞不下我的一个妥协建议是优先保留 FP16 权重把 KV Cache 换成 INT8把 offload 的层数控制在最小。因为权重精度对生成质量的影响远大于 KV Cache 精度这个优先级不要搞反。最后分享一个小技巧是我调优过程中非常受用的每次调参后记录全量环境信息驱动版本、CUDA 版本、vLLM 版本、NCCL 版本、显存预算、并发参数不要只记录数字结果。我至少有三次因为记漏了 NCCL 版本导致两个参数组合看起来一样但性能差一倍白折腾了一个下午。实话说V100 已经老了但它还没老到什么都做不了。写了这么多希望这篇实录能帮到同样还在用老卡折腾大模型的朋友。至少在你准备把 V100 二手卖掉之前可以再试一次说不定性能没你想的那么糟。