拿到一台 V100 的时候我当时心里很清楚Qwen 27B 这模型肯定能跑但跑得快不快完全看你怎么伺候这块 2017 年的老卡。第一次部署完实测只有 4 tok/s输出速度慢到像在“蹦字”。后来花了两周时间做量化选型、换推理引擎、调 KV cache 和批处理最后稳定跑到了 64 tok/s。这个数字已经非常接近 V100 的内存带宽上限再往上基本要靠加卡或者换模型。这篇就把整个调优过程完整复盘一遍从显存账怎么算到每一步为什么这么做适合手里只有单张 V100、又想把大模型真正用起来的人。1. 先算一笔显存和带宽的账再决定优化方向1.1 27B 模型到底要吃多少显存很多人一上来就急着下模型、跑脚本结果第一步就被 OOM 打懵。我要先说一个朴素的结论显存不够一切调参都是白搭。Qwen 27B 一共有 270 亿个参数。按不同精度估算FP16 / BF16270 亿 × 2 字节 ≈ 54GBINT8270 亿 × 1 字节 ≈ 27GBINT4270 亿 × 0.5 字节 ≈ 13.5GB再加上 embedding、lm_head、量化格式自身的冗余一份 4bit 左右的模型文件实际大小大约在 14GB 到 15GB。V100 16GB 版本扣掉驱动和 CUDA context 之后可用显存大约 15GB 左右所以4bit 量化是唯一能整卡塞进去的方案。如果你拿到的是 V100 32GB 版本余地会大很多但下面的推理逻辑完全一样。我当时的第一个错误就是高估了 16GB 的承载能力直接下了个 Q8 的 GGUF 文件文件体积接近 30GB。这种东西天生就不是给 16GB 显卡准备的结果只能靠 CPU 和 GPU 分层加载性能惨不忍睹。1.2 解码速度的物理上限由显存带宽决定大模型推理分两个阶段prefill处理输入和 decode逐个生成 token。decode 阶段的速度几乎完全被“读取模型权重”这件事卡住。每生成一个 token推理引擎都要把模型权重从显存里读一遍。这里的关键参数是 V100 的显存带宽HBM2大约 900GB/s。理论计算很简单4bit 权重大小13.5GB单次读取耗时13.5GB ÷ 900GB/s ≈ 0.015 秒每秒最多生成1 ÷ 0.015 ≈ 66 token/s所以 64 tok/s 这个成绩基本已经摸到 V100 的物理天花板了。换一个角度算如果权重是 FP1654GB ÷ 900GB/s ≈ 0.06 秒每秒最多只有 16 token/s。也就是说哪怕你有一块显存足够大的 V100 32GB只要不做量化FP16 的 decode 速度也上不了 20 tok/s。这个公式比任何采样参数都重要。你先算清楚自己的卡“理论上限是多少”再决定优化手段。如果一开始就跑 4 tok/s不是引擎不够好是你的模型权重根本没放对地方。2. 第一次部署为什么只有 4 tok/s2.1 复盘当时的“常规操作”我第一版部署过程其实很“标准”下载 Q8 的 GGUF 文件用 llama.cpp 的 server 启动显存不够就加-ngl参数把部分层放到 GPU剩下的层放 CPU。当时我的命令大致是这个意思./llama-server -m Qwen3-27B-Q8.gguf \ -ngl 20 \ --ctx-size 4096-ngl是--n-gpu-layers的简写表示把多少层放到 GPU。20 层听起来不少但 Qwen 27B 总共六十几层剩下四十多层全部落在 CPU 上。结果每生成一个 token都要把 CPU 里的那部分权重读出来再通过 PCIe 传到 GPU。这就导致了一个很尴尬的局面GPU 大部分时间在“等待”CPU 的内存带宽成了绝对瓶颈。PCIe 3.0 x16 的实际带宽只有 16GB/s 左右DDR 内存带宽也没高到哪里去。最后测出来就是 4.2 tok/s一个我至今记忆犹新的数字。2.2 CPU offload 的问题不在算力而在搬运很多人误以为 CPU offload 慢是因为 CPU 算力太弱。其实在这种场景里CPU 的瓶颈是“带宽”和“搬运”不是“算力”。每生成一个 token需要把当前层权重从内存搬到显存。40 层 CPU 权重如果是一份 4bit 精度大概有 8GB 到 10GB。即使内存带宽按 25GB/s 算单次搬运也要 400ms 左右换算下来就是每秒 2 到 3 个 token再加上 GPU 上那 20 层的计算、Attention 开销整体叠到 4 tok/s 非常合理。这个阶段给我的教训是看内存占用没有意义要看权重到底放在哪个设备上。只要模型权重没有全部驻留 GPUdecode 速度就不可能好。-ngl不是越高越好如果调太高导致 OOMllama.cpp 会自动把一部分数据挪回 CPU速度反而更差。3. 模型瘦身量化选型比任何调参都优先3.1 AWQ、GPTQ、EXL2、GGUF 怎么选当确定 V100 必须用 4bit 量化之后下一个问题就是选哪种量化格式。这步踩过的坑最多也最值得展开说。先说结论单卡 16GB 的 V100我个人最推荐 EXL2 4bit 左右的文件其次是 AWQ/GPTQ最后才是 GGUF。原因很简单不同格式配不同引擎性能和显存开销差别很大。格式典型精度主要引擎我的评价GGUFQ4_K_S / Q4_K_Mllama.cpp兼容性最好但单卡性能略低AWQ4bitvLLM / Transformers服务化友好适合并发场景GPTQ4bitexllamav2 / vLLM老牌量化工具链成熟EXL2可变 2bit-8bitexllamav2单卡低显存最佳速度最快V100 的算力是 sm_70。很多针对 Ampere、Hopper 新卡优化的量化内核在 V100 上要么跑不了要么会回退到通用的 kernel性能打折。EXL2 的好处是 exllamav2 这个引擎对老卡支持很到位而且支持按位宽精细控制文件大小。如果你不想折腾直接找现成的Qwen3-27B 的 EXL2 4bit文件文件总大小控制在 14GB 左右。如果找不到合适的也可以拿官方权重自己转换exllamav2 仓库里提供转换脚本这个后面会讲。3.2 V100 上必须避开的精度坑V100 这代卡只有 FP16 的 Tensor Core不支持 BF16更不支持 FP8。很多调优文章建议“BF16 推理”但放到 V100 上就会出问题要么引擎自动回退到 FP32显存翻倍速度暴跌要么直接报算子不支持。所以在 V100 上所有推理引擎的 dtype 都建议显式指定为float16不要让框架自动选择。另外如果你自己从源码编译算子记得加环境变量export TORCH_CUDA_ARCH_LIST7.0不然编译器可能默认生成 sm_80 或 sm_90 的代码V100 根本跑不起来或者只能走 JIT 二次编译启动时间极长。4. 推理引擎选型与启动参数实测4.1 llama.cpp、exllamav2、vLLM 的取舍量化文件准备好之后下一步是选推理引擎。我当时把三个主流方案都试了一遍最终留下 exllamav2。llama.cpp最省心GGUF 直接跑--flash-attn也能开。但它对单卡老 GPU 的极致性能挖掘不如 exllamav2。exllamav2本身就是为量化模型推理设计的单卡低显存场景非常强Attention 内核也做了不少优化。缺点是功能偏轻量想搞复杂服务需要自己包一层。vLLM并发能力最强有连续批处理和 PagedAttention非常适合 API 服务。但 V100 的老架构需要编译适配16GB 显存下内存余量也偏紧更推荐 32GB 版本使用。我最后跑出一个很直接的对照配置引擎单流平均 tok/sQ4 量化文件 全层 GPUllama.cpp大概 20-25EXL2 4bit 文件exllamav2大概 38-45AWQ 4bit 文件 并发vLLM单流 35-45并发总吞吐很香如果你只追求“单请求生成速度快”exllamav2 是首选。如果你要开给多个人用vLLM 的总吞吐优势会更明显。4.2 三个引擎的启动方式对比llama.cpp 的启动命令很经典关键是-ngl必须给够让所有层都进 GPU./llama-server -m Qwen3-27B-Q4_K_S.gguf \ -ngl 999 \ --ctx-size 4096 \ --flash-attnvLLM 的启动命令也比较标准注意显式指定float16vllm serve /models/Qwen3-27B-AWQ \ --dtype float16 \ --quantization awq \ --tensor-parallel-size 1 \ --max-model-len 4096 \ --gpu-memory-utilization 0.9exllamav2 我直接用 Python 接口来启动核心逻辑是把模型加载进显存同时给 KV cache 设置一个有限的容量from exllamav2 import ( ExLlamaV2, ExLlamaV2Config, ExLlamaV2Cache_Q8, ExLlamaV2Tokenizer, ) config ExLlamaV2Config(/models/Qwen3-27B-EXL2-4bit) config.max_seq_len 4096 config.gpu_split [16.0] # 单张 16GB V100全部给 GPU model ExLlamaV2(config) cache ExLlamaV2Cache_Q8(model, max_seq_lenconfig.max_seq_len) tokenizer ExLlamaV2Tokenizer(config) model.load_autosplit(cache)具体 API 以你安装的 exllamav2 版本为准但重点就三个模型路径、max_seq_len、cache 类型。后面两个参数直接决定显存够不够。提示如果load_autosplit时爆显存优先把max_seq_len从 8192 降到 4096再把 KV cache 类型从 fp16 换成 Q8。别一上来就调gpu_split那通常是急救手段。5. 从 24 冲到 64真正起作用的四个开关5.1 权重全部驻留 GPU并给 KV cache 留固定预算这一步是基础没有它后面全白搭。权重只有全部常驻显存解码速度才能稳定在内存带宽决定的区间里。我当时的做法是先把模型文件大小压到 14GB 以下然后通过gpu_split把权重锁定到显卡上不允许任何层跑到 CPU。显存剩余空间很少所以必须给 KV cache 设一个固定预算而不是让它无限制增长。KV cache 的大小和max_seq_len直接相关。模型越大、上下文越长KV cache 越占地方。在 V100 16GB 上我直接把max_seq_len锁死在 4096。短上下文场景下这个长度完全够用真要处理超长文档我会换一台显存更大的机器而不是在 V100 上硬撑。5.2 KV cache 量化显存不够时最划算的交换KV cache 默认是 FP16但大部分场景下把它压成 INT8 甚至 INT4对生成质量的影响很小显存却能省下一大半。我用 exllamav2 时把 cache 从默认的 FP16 换成了ExLlamaV2Cache_Q8。单条对话里基本察觉不到效果差异但在显存只有 16GB 的情况下这个操作能多出不少余量给批处理。实测下来的变化非常明显FP16 cache 时显存余量太小稍微开长一点上下文就 OOM换成 Q8 cache 之后不仅不 OOM生成速度也稳了。KV cache 量化的取舍我给一个参考原则聊天、代码、日常问答Q8 完全够长文档、逻辑很强、需要精准复述的任务用 FP16同时缩短max_seq_len或减少并发极限压显存Q4但谨慎使用5.3 FlashAttention 这类算子优化能省显存还能提速度V100 的年代比较早但依然能享受一部分现代算子优化。比如 FlashAttention可以把 Attention 计算里的中间激活值压到很低同时降低显存占用变相给模型更多空间。在 llama.cpp 里直接--flash-attn就能开在 exllamav2 里官方构建通常已经默认启用相关优化。要特别注意的是老卡必须用匹配 sm_70 的算子版本否则要么启动报错要么性能反而不如普通实现。我当时在编译 exllamav2 时踩过一次坑默认没指定 CUDA arch结果所有的 kernel 都按 sm_80 重新编译V100 一个都用不了最后设了TORCH_CUDA_ARCH_LIST7.0重新编译才正常。5.4 并发与批处理让 64 tok/s 真正变成可用吞吐到这里单条流的速度已经到 58 tok/s 左右。最后一个提升到 64 tok/s 的关键不是继续压单条流而是让服务器同时处理多个请求。vLLM 的连续批处理、前缀缓存或者 exllamav2 服务层自带的多用户排队都能做到“一批请求共享一次权重读取”。当多个请求同时运行时单 token 的权重读取成本被摊薄整体吞吐会比单流更高。打个比方一个人去食堂打饭每次都要走一遍全程十个人排好队一起打饭食堂阿姨的接待效率就上来了。在我的实测里服务端开连续批处理之后并发 4 到 8 个请求总体 token 吞吐稳定在 64 tok/s 左右单请求延迟没有明显劣化。这个成绩对一台老 V100 来说已经非常理想。6. 调优过程中的几个意外与最终配置6.1 为什么不是 128 tok/s有朋友问过量化之后文件才 14GB按理说不应该跑到 128 tok/s 吗答案还是带宽。14GB 权重900GB/s 带宽单次读取就要 15ms 左右换算过来就是 66 tok/s 的理论上限。这个瓶颈不在算力也不在引擎而在 HBM2 的物理带宽。除非你继续把精度压到 2bit或者把模型砍到 14B 以下否则 V100 上 27B 模型的单流速度基本就到 60 多。想要更快只有两条路换带宽更高的卡或者用多卡张量并行把权重分散到多张卡上同时读。6.2 几个看似无关却很影响结果的小参数调优过程中我发现除了量化格式和引擎下面几个细节也特别容易拖后腿max_model_len和max_seq_len设置过高显存被 KV cache 提前占满系统频繁触发 swap速度掉到十几。没有关闭长上下文扩展Qwen3 本身支持很长的上下文但在 V100 上这等于给自己挖坑。没有设置前缀缓存每次对话都重新计算 system prompt白白占用 prefill 时间总吞吐上不去。写了太多采样参数do_sample、top_p、temperature 本身不影响速度但如果你开了 beam search代价会成倍上涨。6.3 我最终稳定运行的配置最后给出我留下的稳定配置供参考模型Qwen3-27BEXL2 格式文件大小约 14GB引擎exllamav2dtypefloat16上下文长度4096KV cacheQ8 量化CUDA archsm_70 编译服务端开启连续批处理允许并发 8 个请求总吞吐64 tok/s 左右单流速度也能到 60 上下这套配置在我这边已经稳定跑了一段时间生成质量、响应速度、显存占用都符合预期。如果你手里也正好有一块 V100建议不要上来就急着下模型先算一遍显存账再选量化格式最后决定引擎。顺序反了就会和我一样先收获一个 4 tok/s 的惨案。