
1. 项目背景一块 V100 和 27B 大模型的组合1.1 需求画像为什么非要拿 V100 跑大模型先交代一下背景。我在项目里拿到一批存量 V100 32GB想做一个私有化部署的大模型问答服务模型选型定在 Qwen 27B 的指令版本。第一反应是这组合能跑吗27B 模型光是 FP16 权重就要 54GB 左右一张 V100 满配才 32GB连司机的腿都放不下。但换个思路——把权重量化到 4-bit 左右Q4_K_M 格式大约 17GB这就有机会塞进 V100 了。V100 是 Volta 架构的老卡发布于 2017 年compute capability 是 7.0SXM2 版本有 32GB HBM2显存带宽约 900GB/s。它不是不能跑大模型只是特别挑场景。它没有像 Ampere 那样的 BF16 支持INT8 Tensor Core 也很弱所以在 V100 上跑大模型能用的是 FP16 Tensor Core 和它的高带宽 HBM2。换句话说V100 就是个“内存带宽很强但没有新指令集”的显卡用对工具照样能干活。这篇文章面向谁两类人。一类是手里有存量 V100、被要求“本地跑个大模型”的运维和算法工程师另一类是刚开始接触本地部署想搞明白量化、KV Cache、Flash Attention 这些参数到底怎么影响速度的新手。我会把从 4 tok/s 到 64 tok/s 的每一步、每个参数为什么这么调、每个坑是什么全写出来。1.2 速度指标到底意味着什么4 tok/s 是什么体验你说一句“你好”模型要酝酿将近一秒才吐第一个字然后一个字一个字往外蹦一分钟只能憋出 240 个 token相当于一条中等长度的微信长文。这种速度做聊天是折磨做代码补全更不可能Agent 场景里一个工具调用来回能等半分钟。64 tok/s 是什么体验这就是目前商业 API 中等速度档位的感觉读到流畅聊天基本无感。首 token 延迟从刚才的 1.5 秒降到 100 毫秒级别。如果说 4 tok/s 只能做离线批处理那么 40 tok/s 已经可以支撑小规模实时交互了。我最终的配置是这样的Qwen 27B 指令版Q4_K_M 量化单张 V100 32GB上下文控制在 4096KV Cache 用 Q8_0 量化短上下文峰值能到 64 tok/s长对话稳定在 50 tok/s 左右。接下来就一步步拆解这个过程。2. 为什么速度能提高这么多先搞清楚瓶颈2.1 自回归解码的本质是“显存带宽游戏”大模型生成 token 是自回归的每生成一个 token都要把模型的所有权重完整读一遍做一次前向传播。这个过程中计算量虽然不小但在显卡上通常不是瓶颈真正卡脖子的是显存带宽。一块显存带宽 900GB/s 的 V100如果模型权重是 27B 的 FP16也就是约 54GB那么理论上每秒钟最多读取 16 遍权重也就是说 decode 速度的极限大概是 16 tok/s。你看哪怕是最原始的全精度理论极限也没低到 4 tok/s所以一开始跑出 4 tok/s问题一定出在别的地方。换成 Q4_K_M 量化之后权重降到 17GB 左右。900GB/s 除以 17GB理论极限大约是 52 tok/s。再算上 KV Cache 的读取和 kernel 自身开销实际能跑到 45~55 tok/s 已经非常正常。我最终测到的长对话稳定值在 50 左右短上下文瞬时峰值能冲到 64是因为量化权重的有效读取量在某些 kernel 路径下会更低加上预填充和解码阶段有重叠卡得好一点能到 60 以上。这个数字可能不同版本有差异但量级是对的。所以这次调优的核心思路就一条把每生成一个 token 必须读的数据量压到最低并确保这些数据都在显存里而不是在 CPU 内存里。后面所有参数调整都是围绕这个思路展开的。2.2 量化档位选型不是越低越好我先把不同量化档位在 27B 模型上的体积列个大概注意不同模型和量化实现会有浮动但量级可以参考。量化档位文件体积约相对 FP16 质量解码速度参考V100 32GB 能否全量放显存Q8_0约 28GB接近无损相对较慢勉强需压缩上下文和 KVQ6_K约 22GB很好中等可以Q5_K_M约 19GB推荐档中等偏快可以Q4_K_M约 17GB日常够用最快档之一可以Q3_K_S约 13GB有可见下降快可以适合 16GB 卡Q4_K_M 是我的最终选择。它在 27B 这个规模上数学、代码、中文理解都还能保持不错的水平文件又足够小解码速度快。Q5_K_M 质量更保险但文件要大 2GB 左右带宽占用多 10%速度会降到 45~50。Q8_0 基本无损可 28GB 权重加上 KV Cache 和 CUDA 上下文之后32GB 显存非常紧张实际速度反而不快因为你得把上下文砍到很小还要放弃很多优化空间。有人会问为什么不用更低位的 Q3。我的看法是如果显存实在不够Q3_K_S 是 16GB 卡的最后选择但日常使用能明显感觉到模型“变笨”尤其是一段话里的逻辑推理和长文本约束容易出现漏细节。所以只要显存够我建议至少 Q4_K_M。3. 环境准备llama-server 版本和 CUDA 编译3.1 推理框架选型为什么是 llama.cpp 而不是 vLLM在 V100 上部署 27B 模型框架选择很重要。vLLM 是很火但它在 Volta 架构上并不是最优解。V100 不支持 BF16INT8 Tensor Core 也基本等于摆设vLLM 很多新特性需要较新的 GPU 架构强行在 V100 上编译 FlashAttention 支持会折腾很久而且单卡小显存场景下vLLM 的显存管理优势发挥不出来。ExLlamaV2 也很强但它更偏向新架构的量化推理对 V100 的兼容性一般。所以最后我选了 llama.cpp 的 llama-server。这套方案的好处是GGUF 量化格式在 CPU/GPU 上都有完善实现对老架构的 CUDA 适配做得很好而且内置 OpenAI 兼容 API部署完直接能用 HTTP 接口调用。llama.cpp 版本我建议直接用较新的 release 分支。GGUF 格式一直在演进太老的二进制可能加载不了新模型太新的模型也可能要求升级代码。选一个稳定版本然后在部署脚本里固定 commit 号避免过段时间拉更新后行为变化。3.2 从源码编译出 V100 可用的 llama-server有人会直接用官方预编译的 llama-server但那个包不一定包含 CUDA 支持或者没针对 V100 编译。因为在 cmake 阶段如果不指定架构构建脚本可能在 GPU 检测时选错目标或者只编译一个通用版本导致 V100 上性能很差甚至跑不起来。我的做法是源码编译并且明确指定 CUDA 架构。git clone https://github.com/ggerganov/llama.cpp cd llama.cpp cmake -B build \ -DGGML_CUDAON \ -DCMAKE_CUDA_ARCHITECTURES70 \ -DCMAKE_BUILD_TYPERelease cmake --build build --config Release -j $(nproc)这里最关键的是CMAKE_CUDA_ARCHITECTURES70对应 V100 的 compute capability 7.0。不写的话cmake 可能会编译出只适用于当前驱动默认架构的 kernel结果在 V100 上要么报no kernel image available要么明明装了 CUDA 却一直在用 CPU 跑。编译完成后可执行文件在build/bin/llama-server和build/bin/llama-bench。llama-bench后面做基准测试会用到。还有个小坑编译前先确认驱动和 CUDA 版本。我用的是 CUDA 12.x 配比较新的驱动V100 依然在支持名单里。如果你的驱动很老建议先nvidia-smi看一眼驱动版本再决定要不要升级。3.3 GGUF 模型获取下载还是自己转最省事的方式是直接下载现成的 GGUF 文件。HuggingFace 上 Qwen 官方仓库就有 GGUF 格式版本用 huggingface-cli 下载即可。huggingface-cli download Qwen/Qwen2.5-27B-Instruct-GGUF \ qwen2.5-27b-instruct-q4_k_m.gguf \ --local-dir ./qwen27b如果你手上只有 HF 的 safetensors 原始权重也可以自己转。llama.cpp 仓库里自带转换脚本流程是先转 FP16 的 GGUF再量化成目标格式。python3 convert_hf_to_gguf.py /path/to/Qwen2.5-27B-Instruct \ --outfile qwen27b-f16.gguf --outtype f16 ./build/bin/llama-quantize qwen27b-f16.gguf qwen27b-q4_k_m.gguf Q4_K_M自己转的好处是可以控制量化动作坏处是转换脚本和模型格式需要匹配。如果报tokenizer相关错误多半是模型文件里加了新的特殊 token而 convert 脚本版本太老建议更新 llama.cpp 到较新版本再试。4. 第一次部署4 tok/s 是怎么来的4.1 我最初的启动命令和实测表现第一次启动我很偷懒直接敲了这么一条命令./build/bin/llama-server \ -m ./qwen27b/qwen2.5-27b-instruct-q4_k_m.gguf \ -c 8192 \ --port 8080然后进 API 发了一个问题生成速度稳定在 3~5 tok/s首 token 要等 1.5 秒左右整个体验非常痛苦。当时我下意识觉得“V100 跑 27B 果然不行”但转头用nvidia-smi一看显存占用只有 1.2GBGPU 利用率接近 0%CPU 倒是吃满了一半核心。这明显说明 GPU 根本没干活。再看 llama-server 的启动日志里面有一行非常关键load_tensors: offloaded 0/48 layers to GPU0 层在 GPU 上意味着整个模型都跑在 CPU 内存里。那时候我才反应过来llama-server 默认不会把任何层 offload 到 GPU需要显式指定-ngl或者-gpu-layers。CPU 跑一个 17GB 的 Q4_K_M 模型性能上限基本就是 4~8 tok/s因为 CPU 内存带宽再高也就几十 GB/s跟 V100 的 900GB/s 差了一个数量级。4.2 默认参数为什么这么慢这里我把慢的原因拆成几个点方便对照排查-ngl没有设置默认 0模型全在 CPU。-c 8192把上下文开得很大KV Cache 默认是 FP16不仅占显存还占带宽。注意此时连 GPU 都没用上这部分全在内存里。没有开启 KV Cache 量化也没开 Flash Attention潜在优化一个没用。日志里offloaded 0/48说明 GPU 闲置但如果你看日志只看开头几行很容易忽略这个问题。有人可能会说那直接把-ngl拉到 999 不就行了吗在 32GB 的 V100 上大体可以但在 16GB 的 V100 上这么干会直接 OOM。所以下一步要先把显存账算清楚再决定怎么 offload。5. 调优实录把参数一个个“压”到 V100 上5.1 显存预算计算权重、KV Cache 和 CUDA 上下文部署大模型最怕的就是显存溢出。我先算了一笔账。模型权重 Q4_K_M 大约 17GBKV Cache 的大小和上下文长度、模型的层数、KV heads、head_dim 有关。以常用 27B 指令模型为例单 token 的 KV Cache 在 FP16 格式下大约是 96KB 左右。4096 上下文就是 4096 × 96KB ≈ 400MB。如果 KV Cache 用量化格式 Q8_0体积减半大约 200MB。然后是 prefill 阶段的计算缓冲区和 CUDA context这部分要留 1~2GB。所以整体算下来Q4_K_M 权重约 17GB4096 上下文 Q8_0 KV Cache约 0.3GBCUDA 上下文/计算缓冲约 1.5GB合计约 18.8GB在 V100 32GB 上完全放得下剩了 13GB 富余。即使把上下文开到 8192KV Cache 也才 600MB 左右依然没问题。但如果换成 16GB 的 V100这个配置就危险了得走 5.4 节的替代方案。KV Cache 的显存计算没必要背死数关键是记住一个公式KV 越大、上下文越长、精度越高显存越高。想要在显存有限的情况下跑更长上下文第一选择是把 KV Cache 量化第二选择是缩短上下文而不是去换更大的模型。5.2 核心参数逐个解释为什么这样调我把最终用到的核心参数一个个拆开说这些参数在 llama.cpp 的最新版本里都有效。-ngl / --gpu-layers控制多少层放到 GPU。设999表示全部放 GPU。日志里offloaded 48/48就是全量 offload。这一步是速度从 4 提到 50 的根本原因GPU 和 CPU 之间每多一层跨设备传输速度就会掉一截。--flash-attn onFlash Attention 可以显著降低解码时 KV Cache 的读取压力。V100 不是 Ampere 新卡但新版 llama.cpp 已经适配了 Volta 的 FA kernel。开启后显存占用和速度都有改善。如果编译版本不支持启动时会报错这时候建议重新编译而不是关掉。--cache-type-k / --cache-type-v设置 KV Cache 的数据类型。我用的q8_0质量影响很小显存省一半。也有人用q4_0但我试下来在长上下文任务里偶有质量下降所以保守选 Q8_0。27B 模型本身对 KV 精度不是特别敏感但关键任务还是别选太激进。-c / --ctx-size上下文长度。我最终用 4096而不是一开始的 8192。上下文越长KV Cache 占用越大解码时每次读取的数据也越多速度会下降。如果业务对长上下文要求不高4096 是一个性能和质量比较平衡的值。对于 RAG 场景如果你只喂一小段文档那 2048 也够速度还能再快一点。--batch-size 和 --ubatch-size这两个参数影响 prefill 阶段的速度。prefill 就是用户输入的那一段模型需要一次性并行处理所有输入 token。batch-size 设太小prefill 慢设太大显存爆。V100 上我试了 512 比较稳256 也能跑但 prefill 稍慢1024 在某些配置下会 OOM。注意这两个值不是越大越好要结合显存。--threads 和 --threads-batchCPU 线程数。即使 GPU 推理模型加载、tokenizer、采样等环节也会用到 CPU设置过多反而会互相抢资源。我按物理核数的一半设了 8效果比默认值好。--mlock 和 --no-mmap把模型文件锁在内存里避免被 swap 到磁盘。对纯 GPU 推理影响不大但做部分 offload 时必须开。我实际部署时开了--mlock没开--no-mmap因为--no-mmap会一次性把所有文件读进内存再传给 GPU启动慢且占用更大内存。--parallel并发槽位。默认 1每个并发槽位都会额外分配一份 KV Cache。单卡 V100 做演示我建议保持 1否则并发请求会互相拖慢。如果并发需求高显存又有限还不如用 Nginx 在 upstream 上做多实例负载均衡。5.3 最终启动命令和实测数据最后稳定的启动命令是这样的./build/bin/llama-server \ -m ./qwen27b/qwen2.5-27b-instruct-q4_k_m.gguf \ --host 0.0.0.0 \ --port 8080 \ -ngl 999 \ -c 4096 \ --flash-attn on \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ --batch-size 512 \ --ubatch-size 512 \ --threads 8 \ --threads-batch 8 \ --mlock \ --parallel 1启动后日志里能看到offloaded 48/48 layers to GPU显存占用大约 19GBGPU 利用率在对话过程中稳定在 80% 以上。我用一个固定问题集测了 100 轮生成统计结果是这样的配置阶段上下文长度KV Cache 类型decode 速度参考初始默认无 -ngl8192FP164 tok/s全量 GPU 8192 FP16 KV8192FP1630~35 tok/s全量 GPU 4096 Q8_0 KV FA4096Q8_045~55 tok/s最终优化 短上下文任务2048Q8_0峰值 64 tok/s测试时别只看聊天页面里的速度估算最好用llama-bench做统一基准。./build/bin/llama-bench \ -m ./qwen27b/qwen2.5-27b-instruct-q4_k_m.gguf \ -p 128 -n 128 -r 5它会分别测预填充速度和生成速度可以输出平均 tok/s比肉眼估算靠谱。这里提醒一句长对话生成的后期KV Cache 越来越大单 token 要读的 KV 也越来越多速度会略降。所以标称 64 是短上下文峰值长对话稳定在 50 左右是很正常的。5.4 如果只有 16GB 的 V100 怎么办很多人的 V100 其实是 16GB 版本。这种情况下 Q4_K_M 的 17GB 权重塞不下我的建议是换 Q3_K_S 量化文件大约 13GB全量 offload 后还能留出给 KV Cache 和上下文的显存空间。./build/bin/llama-server \ -m ./qwen27b/qwen2.5-27b-instruct-q3_k_s.gguf \ -ngl 999 \ -c 2048 \ --flash-attn on \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ --mlock \ --port 8080如果你就是不想牺牲质量坚持 Q4_K_M那就只能部分 offload。例如把-ngl设为 40也就是 48 层里 40 层放 GPU、8 层放 CPU。这样显存占用大概 14GB 多速度能到 15~25 tok/s。这个数字不惊艳但比一开始的 4 tok/s 好太多。再往上调-ngl很容易 OOM需要反复试。我个人的建议是16GB V100 上如果业务要求实时交互优先 Q3_K_S如果离线批处理能容忍慢一点那 Q4_K_M 部分 offload 更划算。6. 生产接入与常见问题6.1 常见问题速查表部署和调优过程中我踩过的坑不少整理成一张表方便排查。现象原因解决方案日志显示 offloaded 0/48没加-ngl启动命令加-ngl 999CUDA error: out of memory权重或 KV Cache 超显存降低-c、KV 用 Q8_0、换小量化档提示系统资源不足、无法完成 API显存被其他进程占满或容器配额不够nvidia-smi查占用kill 残留进程容器检查--gpus配置加载模型报 not a valid modelGGUF 文件与二进制版本不匹配升级或回退 llama.cpp 版本开启 Flash Attention 报错编译时没有包含对应 CUDA kernel用-DCMAKE_CUDA_ARCHITECTURES70重新编译模型在 GPT/对话时乱码采样参数极端或 tokenizer 版本不对恢复默认采样参数更新模型和代码版本显示 GPU 利用率高但速度仍慢部分层还在 CPU跨设备传输频繁查看 offloaded 层数尽量全量 offload访问时报 self_signed_cert_in_chain用 HTTPS 访问本地服务证书校验问题改成http://127.0.0.1:8080或配置证书信任关于“系统资源不足”这类问题我第一次遇到也很懵最后发现是另一个 Python 进程占着 14GB 显存没释放。nvidia-smi能看到进程列表必要时用fuser -v /dev/nvidia*找到占用进程确认后清理掉再启动服务。6.2 OpenAI 兼容 API 和 Qwen Code 接入llama-server 启动后本身就兼容 OpenAI 的/v1/chat/completions接口接入成本极低。用 curl 测一下curl http://127.0.0.1:8080/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen-27b, messages: [ {role: user, content: 用一句话介绍你自己} ], temperature: 0.7, max_tokens: 256 }注意这里model字段随便填llama-server 不校验真正起作用的是启动时加载的模型文件。如果用 Pythonopenai SDK 只需要改base_urlfrom openai import OpenAI client OpenAI( base_urlhttp://127.0.0.1:8080/v1, api_keynot-needed ) resp client.chat.completions.create( modelqwen-27b, messages[{role: user, content: 你好}], temperature0.7 ) print(resp.choices[0].message.content)有些工具在配置本地 API 时会默认填 HTTPS比如 Qwen Code 或其他客户端如果它报connection error: self_signed_cert_in_chain大多数时候不是服务端问题而是客户端用 HTTPS 访问本地 HTTP 服务证书校验失败。把地址改成http://127.0.0.1:8080/v1或者关掉客户端的证书校验问题就消失。6.3 还能继续挖的性能空间这套配置跑稳之后还可以继续优化。比如 V100 支持多卡llama.cpp 可以用--split-mode layer或row做多卡并行两张 V100 拆模型权重速度还能再往上走。但多卡部署的显存带宽是分摊的层切分会有跨卡传输开销不一定线性提升得自己测。另一个方向是 speculative decoding用一个小模型做 draft大模型做 verifyV100 上如果配合得好短文本生成可以提高 1.5~2 倍。llama.cpp 的 API 支持--draft参数但 draft 模型也需要额外显存要做取舍。如果应用场景是 RAG不要盲目追求长上下文。用户问题加检索片段一般也就 1000~2000 token把上下文控制在这个范围每一轮生成都快不少。说白了性能瓶颈不在模型能不能更长而在你的业务到底需不需要那么长。最后再分享一个我个人的经验调优大模型部署第一件事永远是看显存带宽和显存占用而不是急着调采样参数和 prompt。先算权重要多少、KV 要多少、CUDA 上下文要多少再决定量化格式和上下文长度。我一开始就是没算账结果 4 tok/s 跑了大半天。后来把账算明白参数一套上速度直接起飞。这块被很多人嫌弃的 V100其实还能在存量硬件上发挥很大价值关键是别让它闲着。