1. V100 的硬件账为什么老卡不是不能跑而是要把账算清楚1.1 参数读取的带宽账本先说结论大模型推理在多数时候根本不是“算力不够”而是“参数搬不过来”。生成式模型的每一步 token理论上都需要把模型权重完整读一遍后做矩阵乘法。Qwen 27B 如果以 FP16 存储权重大约是 54GB如果以 Q4 量化存储大约只有 15~16GB。V100 的显存带宽是约 900GB/s。54GB 除以 900GB/s每次生成大约 60 毫秒理论上限也才 16 token/s。而 15GB 的量化权重理论上限能做到 50~60 token/s 甚至更高。也就是说量化这件事不是“牺牲精度换内存”的取舍它直接砍掉了推理时最要命的显存带宽压力。这也是为什么“4 到 64 tok/s”听起来有点玄实际却能发生只要瓶颈从 PCIe 和 CPU 搬运变成纯 GPU 推理数字就会彻底换一个量级。1.2 V100 缺的那些“现代能力”V100 是 Volta 架构SM 7.0发布于 2017 年。很多人以为它“能跑 CUDA 就能跑现代大模型”这太乐观了。它有三个明显短板不支持 bf16 原生计算。现代 vLLM、SGLang、甚至部分 HuggingFace 推理脚本默认会用 bf16V100 直接不支持得显式切到 FP16。FlashAttention 的支持非常有限。FlashAttention-2 主要面向 Ampere 及以后架构在 Volta 上要么不可用要么退回普通 attention功能上没问题性能提升和 RTX 3090 上的效果没法比。没有 INT4/INT8 Tensor Core。Turing 之后的 GPU 支持低精度张量核心Volta 时代只有 FP16 Tensor Core。所以很多人以为“量化权重在 V100 上特别快”实际还是要先反算回 FP16 再做矩阵乘。这些缺点的直接后果是现代的推理框架默认配置基本都不是为 V100 调过的。1.3 16GB 显存和 27B 模型之间的水位27B 模型的“真实身材”大概是这样存储格式理论占用16GB V100 能不能装FP16 / BF16约 54GB完全没戏INT8 / Q8约 27GB没戏Q5_K_M约 18GB勉强但基本没有 KV 缓存空间Q4_K_M约 16.3GB很悬上下文只能开很小Q4_K_S约 15.2GB可以剩余空间留给 KV 与计算图Q3_K_M约 13.5GB宽裕质量略有损失我自己最后选了 Q4_K_S。不是因为 Q4_K_M 不好而是 16GB 的 V100 必须给 KV cache 和运行时留余量。为了挤进 16GB 反而频繁 OOM调起来特别痛苦。2. 从 4 tok/s 开始第一次部署的完整错误示范2.1 错误示范默认配置下的表现我第一次拿到这块卡的时候想当然地认为“llama.cpp 下载下来就能直接跑”。于是我用编译好的默认版本直接执行了类似这样的命令llama-server -m /models/qwen-27b-Q8_0.gguf \ -ngl 20 -c 32768结果就是经典的 4 tok/s 左右而且生成稍微长一点的内容还会越来越卡。原因其实不复杂-ngl 20意味着 64 层模型里只有 20 层放进 GPU剩下 44 层在 CPU 上跑。Q8_0 格式的 27B 模型权重大概 27GB每次生成 token 都要从系统内存或 PCIe 搬运大量数据。上下文开成 32768KV cache 占用又额外吃掉巨量内存CPU 那侧的 brunt 更重。这个 4 tok/s 不是 GPU 算不动而是被 CPU offload 和 PCIe 带宽卡死了。2.2 用最土的办法定位瓶颈不要一上来就优化参数先把瓶颈确认了。我自己常用的是这几个命令nvidia-smi看显存占用、GPU 利用率、功耗。当时 GPU 利用率只有 30% 不到显存占用也没满功耗低得离谱这基本说明 GPU 在“等数据”。nvtop更好用能实时看显卡读写的负载但需要额外安装。如果能看到显存控制器占用率接近 100%说明显存带宽已经吃满再堆算力也没用如果显存控制器占用很低那瓶颈就在别的地方。llama-server 启动时也会打印 offload 信息比如llm_load_tensors: offloading 20 layers to GPU llm_load_tensors: offloaded 20/64 layers to GPU看到这种日志就别自欺欺人了权重根本不在 GPU 上性能差是必然的。2.3 吸教训先让权重整体落显存那次之后我定了一个原则任何 27B 模型在 16GB 的 V100 上第一步不是选多好的量化而是先把全量权重放进显存。哪怕用 Q4_K_S 让显存紧一点也比用 Q8 然后 offload 20 层要快得多。计算顺序必须是量化等级调到能完整放下全部权重。上下文长度调到 KV cache 不炸。再谈 FlashAttention、并行、投机解码这些优化。顺序反了后面全是白忙。3. 量化选择让 27B 真正住进 16GB 显存3.1 GGUF 的 Q4_K_S 为什么是 V100 甜点GGUF 是 llama.cpp 生态的标准格式V100 支持度最好。相比 GPTQ 和 AWQGGUF 的好处是可以随时在 Q3、Q4、Q5 之间切换而且 KV cache 量化和层 offload 支持得最好。GPTQ/AWQ 的 4bit 模型权重虽然也能压到 14GB 左右但 vLLM 在 V100 上跑这两个格式时量化反算容易触发不兼容路径反而慢Llama.cpp 对 GGUF 的处理更成熟。建议直接用下面的表格对齐参考量化等级权重体积约效果V100 16GB 推荐度Q3_K_M13.5GB能用复杂逻辑略有下降如果一定要开很长的上下文可以选Q4_K_S15.2GB质量很好和原版差距较小最推荐Q4_K_M16.3GB质量略好一点但显存紧需要 Q4_K_S 和它的差价来换空间Q5_K_M18GB质量更稳不推荐塞不下我最终用 Q4_K_S配合 8K 上下文单流生成能稳定在 18~22 tok/s 左右。这个数字看起来不够“标题党”但它是后期并发吞吐的地基。3.2 KV cache 量化到底省了多少很多教程不提 KV cache 量化但在 16GB 的 V100 上这个操作极其关键。上下文长度越长KV cache 占用就越大。Qwen 27B 本身是 GQA 架构KV 缓存比 MHA 架构小不少但 8K 上下文下也还是要吃掉 1~2GB 的显存。llama.cpp 从某个版本开始支持-ctk和-ctv参数把 Key 和 Value 缓存量化成 8bitllama-server -m /models/qwen-27b-q4_k_s.gguf \ -ngl 99 \ -c 8192 \ -ctk q8_0 \ -ctv q8_0这样 KV cache 的体积大约能降一半。有人测试过 q8_0 的 KV cache 对回答质量影响极小至少在长文本任务里我肉眼没看出差别。如果你把上下文开到 16KKV cache 量化几乎是强制的否则很容易 OOM。注意一点-ctk q4_0这种更激进的选择虽然能进一步省显存但我在 V100 上遇到过输出质量下降的情况所以我能不用就不用。3.3 上下文长度劝退128K 在 V100 上是灾难Qwen 27B 本身宣传支持很长上下文但在 16GB 显存下这基本是空话。模型权重已经吃掉 15GB 左右的显存剩下的空间再表现也很有限。我的经验是4K 上下文绝大多数 Chat 场景和文档问答够用。8K 上下文配合 KV 量化还能稳定跑。16K 上下文显存会拉满有空可能 OOM不建议生产使用。128K看看就好。对 V100 16GB 来说8K 已经是很舒服的平衡点。很多人一上来就开 32K结果 OOM 以后还怪量化不好其实是没考虑内存账。4. 从单流 18 到并发 64编译、FlashAttention 和连续批处理4.1 给 V100 单独编译 llama.cpp如果你直接下载官方预编译的 llama.cpp 二进制默认会包含大量 GPU 架构的二进制扩展比如 sm_80、sm_86、sm_90。V100 是 sm_70用预编译包也能跑但很多地方会走 PTX 兼容路径或 fallback启动慢个别 kernel 也不是最优。我为 V100 单独编译了一次git clone https://github.com/ggerganov/llama.cpp cd llama.cpp cmake -B build-v100 -DGGML_CUDAON \ -DCMAKE_CUDA_ARCHITECTURES70 \ -DGGML_CUDA_FA_ALL_QUANTSON \ -DCMAKE_BUILD_TYPERelease cmake --build build-v100 --config Release -j关键就是-DCMAKE_CUDA_ARCHITECTURES70。这一行能让所有 CUDA kernel 都按 V100 的 Volta 架构编译少掉一堆动态判断启动时间从几十秒降到几秒生成阶段也会有点提升。GGML_CUDA_FA_ALL_QUANTS是为了让 FlashAttention 或者相关内核在部分量化权重下也能开启但在 V100 上效果有限属于锦上添花。4.2 FlashAttention 的开关能开就开不能开也别强求llama.cpp 的-fa on理论上能减少 attention 部分的内存读写对长上下文很有帮助。但在 V100 上FlashAttention 的支持不像 Ampere 上那么顺。如果启动日志里出现类似“flash attention is not supported on this GPU”的提示或者生成速度反而变慢就关掉它。不会有灾难性后果只是 attention 部分回到传统实现而真正吃掉时间的是权重读取和矩阵乘法不是 attention。vLLM 里也类似。如果你用 vLLM 跑 Qwen 27B不指定任何参数时它会尝试 FlashAttentionV100 上很可能会报错。加这两个参数vllm serve /models/qwen-27b-gptq \ --quantization gptq \ --dtype half \ --enforce-eager \ --max-model-len 8192 \ --gpu-memory-utilization 0.95--enforce-eager可以避免 V100 上编译或执行某些融合 kernel 时直接失败--dtype half则绕过 bf16 支持问题。4.3 64 tok/s 的秘密并发请求与连续批处理很多人看到 64 tok/s 就先入为主以为单条请求生成速度是 64。其实这张卡上单流生成也就 18~22 tok/s64 tok/s 是并发压力下的聚合吞吐量。为什么单流那么难上升因为自回归生成是有依赖的必须等前一个 token 生成完才能算下一个GPU 就算有很多并行单元单流时也难以全部塞满。并发请求就能把多个请求的 token 塞进同一个 batch让 GPU 的矩阵乘法真正满载。所以合理的压测脚本不是只发一个请求而是同时开 4~8 个流式请求。llama.cpp 的 server 天然支持并行llama-server -m /models/qwen-27b-q4_k_s.gguf \ -ngl 99 \ -c 8192 \ -ctk q8_0 \ -ctv q8_0 \ -np 4 \ -t 8-np 4表示最多 4 条并行序列-t 8是 CPU 线程数实际影响不大但对调度有帮助。在这种配置下我用 4 个并发请求打上去聚合 token 吞吐就接近 60~70 tok/s。vLLM 的连续批处理continuous batching更激进它会在请求流式生成的空隙里插入其他请求的计算如果显存放得下并发数还能更高。4.4 投机解码能加多少投机解码的思路是拿一个小模型先生成候选 token大模型再并行验证如果小模型猜得准就可以减少大模型实际生成的步数。在 V100 上可以用 llama.cpp 的--model-draft参数llama-server -m /models/qwen-27b-q4_k_s.gguf \ --model-draft /models/qwen-3b-q4_k_m.gguf \ --draft-max 4 \ --draft-min 1我实测收益大概有 10%~30%取决于任务类型。如果你的显存已经非常紧还要塞一个 3B 的 draft 模型那就不太划算。显存宽裕再玩它。4.5 测速之前先想清楚你到底在测哪个数字很多人被“64 tok/s”误导测试时用单个 curl 请求结果发现只有 18就以为文章骗人。真不是单流和聚合吞吐是两码事。我自己会把三个指标分开看首 token 延迟模型启动后第一个 token 出来要多快。V100 上大概几百毫秒属于正常。单流生成速度一次流式请求里每秒生成多少个 token。Q4_K_S 8K 上下文下大概 18~22 tok/s。聚合吞吐多个并发请求时服务端每秒总共输出多少 token。4 并发下能到 60~70 tok/s。从 4 到 64严格说是从“单流 CPU offload 的 4 tok/s”走到了“并发 4 路的 64 tok/s”。这两者在生产环境里都很有意义但含义完全不同。5. 实测数据、可直接落地的启动脚本与坑位记录5.1 从 4 到 64 的完整调优路径阶段配置单流 tok/s4 并发聚合 tok/s显存占用初始Q8_0 offload 20 层 32K 上下文4基本无法并发显存占用约 8GB系统内存爆炸第二阶段Q4_K_S 全量 GPU 8K 上下文18~22约 45~55显存约 15.5GB第三阶段Q4_K_S 单独 sm_70 编译 KV 量化 4 并发20~24约 62~70显存约 15.8GB可选进阶再加投机解码22~2870显存会再吃 1~2GB第三阶段就是标题里说的 64 tok/s。注意这个结果是在服务端已经并发跑了一段时间、模型权重全部加载进显存后测出来的稳定值。5.2 可以直接复制的启动命令先编译再启动cmake -B build-v100 -DGGML_CUDAON \ -DCMAKE_CUDA_ARCHITECTURES70 \ -DGGML_CUDA_FA_ALL_QUANTSON \ -DCMAKE_BUILD_TYPERelease cmake --build build-v100 --config Release -j启动 llama-server./build-v100/bin/llama-server \ -m /models/qwen-27b-q4_k_s.gguf \ -ngl 99 \ -c 8192 \ -ctk q8_0 \ -ctv q8_0 \ -np 4 \ -t 8 \ --host 0.0.0.0 \ --port 8080如果要压测聚合吞吐可以用一个很简单的 Python 脚本同时开 4 个流式请求统计 token 数import asyncio, aiohttp, time async def gen(session, prompt): payload { prompt: prompt, max_tokens: 256, stream: True } async with session.post(http://127.0.0.1:8080/v1/completions, jsonpayload) as resp: tokens 0 async for line in resp.content: if bchoices not in line: continue tokens 1 return tokens async def main(): prompts [写一段关于量子计算的科普] * 4 async with aiohttp.ClientSession() as session: start time.time() results await asyncio.gather(*[gen(session, p) for p in prompts]) elapsed time.time() - start print(f总 token 数: {sum(results)}, 耗时: {elapsed:.2f}s, 聚合吞吐: {sum(results) / elapsed:.1f} tok/s) asyncio.run(main())这段脚本只是示意但测出来的数字会比较接近实际并发能力。5.3 常见报错和补救办法报错/现象原因解决CUDA out of memory权重或 KV cache 超了降量化等级降上下文开 KV 量化FlashAttention not supported / not enabledVolta 架构不支持去掉-fa on不影响基本生成Bfloat16 is not supported on this GPUV100 不支持 bf16加--dtype half或改用 FP16 权重启动很慢或卡在 CUDA init 很久预编译包包含多种架构用CMAKE_CUDA_ARCHITECTURES70重新编译显存没占满但单流速度低可能是权重没有完全上 GPU或没开并发检查日志里 offload 的层数用-ngl 99上下文稍微一长就 OOMKV cache 占满剩余显存减小-c或把-ctk/-ctv调成 q8_0这里我要重点提一个问题很多人遇到 OOM 后会去换更激进的量化等级比如 Q3其实先把上下文砍短效果更直接。8K 上下文的 KV cache 能吃掉 1~2GB4K 上下文直接减半很多时候根本不需要牺牲模型权重质量。6. 最后再分享一点实际操作心得这套配置我跑了一周我的感受是V100 虽然老但用来跑私有化 Qwen 27B 推理是划算的。它最大的本钱是 900GB/s 的显存带宽只要量化到位、权重全部进显存单流速度并不会落后新卡太多短板是不能碰 bf16、FlashAttention-2 这些现代特性所以框架版本不能追新参数也不能全抄 Ampere 卡的教程。另外不要迷信“量化越低越好”。我在 Q3_K_M 和 Q4_K_S 之间对比过复杂指令和长文档摘要场景下Q4_K_S 的稳定性和逻辑性明显更好而 Q3 省下的那点显存只够多开一点上下文性价比不高。如果你和我一样只有一块 V100我建议你先从 Q4_K_S 8K 上下文 KV 量化这套组合开始跑确认稳定后再去调并发。先把基础打牢再谈 64 tok/s 这种数字。还有一个小技巧生产环境下别把显存利用率拉满。留出 0.5~1GB 顶量不然请求高峰时 KV cache 稍微波动就会 OOM整个服务直接崩掉那比慢几 token 惨多了。
