尧图精选

V100实战:27B模型从4 tok/s到64 tok/s的调优全记录

🕒 发布时间:2026/9/20 4:21:39 📁 来源:尧图网络
不用看图表不用看天花乱坠的“一键部署”这篇就是实打实的记录怎么把一块老旧的V100从跑27B模型“卡得没法用”一路调优到服务化场景下64 tok/s的聚合吞吐。整个过程有数据、有命令、有踩坑也有大量的“为什么”。4 tok/s是什么体验你朝对话框丢进去一个问题然后屏幕上的字开始一个词一个词往外蹦慢到你能看清每一个字的“出生过程”。生成一段200字的回复差不多要等五十秒。这种速度别说接进业务当API用自己调试多问几个问题都能把手里的茶等凉。我最初在V100上部署Qwen2.5-27B-Instruct时面对的就是这个数字。后来同一个模型、同一张卡单流解码跑到了38-42 tok/s在4路并发下聚合吞吐稳定在64 tok/s。这篇文章把整个过程完整拆解一遍包括每一步选了什么东西、为什么选、实测数据是多少、有哪些隐藏特别深的坑。1. 先交代背景一块V100为什么非得上27B先说明一下当时的处境实验室能用的GPU只有V100的云主机和机房老卡新卡申请流程太长业务方又点名要试Qwen系列27B这个体量的模型想看它在代码理解任务上的真实表现。硬件条件被锁死模型体量又是刚需剩下的问题只有一个怎么把两者之间的差距填平。这个问题的答案不是“使劲调参”而是先搞明白V100到底差在哪、不差在哪。我见过太多人一看到V100就摇头觉得老卡跑不了现代大模型但其实这个判断并不准确。1.1 V100 16GB的底细V100是2017年的数据中心卡Volta架构计算能力sm_7016GB HBM2显存。刚拿到手时我也觉得它“过时”但把参数表摊开仔细看它并没有想象中那么不堪。重点是显存带宽约900GB/s。作为参照RTX 4090大概是1008GB/sA100 40GB是1555GB/s。V100的带宽是4090的九成放在今天依然够看。大模型解码阶段的核心矛盾在于“把权重从显存里读出来”这件事而不是“计算”本身。生成一个token理论上需要把整个模型的权重从HBM过一遍在batch很小的时候计算单元经常是闲着等数据的。我做了一个粗略估算27B模型做4-bit量化后权重大约15-16GBV100的900GB/s带宽理论上限就是900/15.5约等于58 tok/s。也就是说一张V100跑27B量化模型单流速度冲到三四十是完全有物理基础的问题只在于怎么把理论带宽吃满。V100真正的短板有两个。第一它不支持BF16格式而现在很多模型权重和kernel默认用BF16拿到V100上需要配套处理。第二它的Tensor Core对INT8/INT4的支持很弱新卡那种专门为量化推理加速的硬件单元在V100上指望不上所以不要幻想“换了量化格式就自动快10倍”。实际提速靠的是显存腾挪、kernel优化和并发调度。1.2 Qwen 27B模型的胃口到底有多大我用的模型是Qwen2.5-27B-Instruct标准的Decoder-only结构64层Transformerhidden_size358428个Q头4个KV头GQAhead_dim128。这里埋伏了一个对部署很友好的点GQA让KV头数量只有4个KV cache体量不大在显存紧张时很有价值。模型权重在不同精度下的体量大概如下表精度/量化格式文件大小约是否能塞进V100 16GBBF1655GB否Q8_027GB否Q6_K21GB否Q5_K_M18GB否Q4_K_M16.3GB勉强KV cache空间很少Q4_015.5GB勉强Q3_K_M13.9GB是空间较宽裕EXL2 4.0bpw约15.3GB勉强EXL2 3.3bpw约12.8GB是Qwen2.5-27B这一代模型在BF16下权重约55GB8-bit约27GB6-bit约21GB单卡16GB的V100想都不用想必须上4-bit甚至更小。所以我从一开始就把这次任务定性为一个显存预算问题而不是算力问题。所有调优动作本质上都是在“把模型完整塞进显存”和“给KV cache留出空间”之间找平衡。2. 那个令人绝望的4 tok/s是怎么来的4 tok/s不是某一步偶然踩出来的它是一套“看起来合理实则处处撞墙”的默认配置的必然结果。我第一次部署时用的组合是llama.cpp的一个偏旧版本 Q8_0量化GGUF 默认上下文8192 让llama.cpp自动决定GPU层数。这套配置的结果是模型约27GBGPU只能装下不到10GB的层剩下约17GB权重全部落在CPU内存里。这个组合跑起来每生成一个token都变成了一场“跨设备马拉松”一部分层在GPU上算另一部分层在CPU上算中间结果还要通过PCIe来回搬运。PCIe 3.0 x16的带宽约16GB/s看着不低但大模型推理是高度串行的几十层网络必须一层层算完每一层之间的数据同步都会把延迟拉起来。更致命的是CPU侧要用DDR4内存反复读取十几GB权重。DDR4带宽通常只有四五十GB/s单线程算力也远不如GPU于是CPU侧读17GB权重的理论下限就已经是50/17≈2.9秒每token除以一下就是不到3 tok/s。最终实测4 tok/s还得算上GPU层帮了点忙的结果。定位这个瓶颈其实不难我给了自己三个问题用nvidia-smi看GPU显存和利用率发现显存只用了不到10GB利用率在30%-70%之间反复横跳明显不是GPU吃饱的状态。用llama.cpp的--verbose打印各阶段耗时CPU推理部分的耗时占了单次生成总耗时的六成以上。把上下文从8192降到2048速度几乎没有变化。如果瓶颈在显存或KV cache这一步应该会有明显影响但结果纹丝不动说明根本没跑在显存受限区。到这一步已经可以确认4 tok/s的罪魁祸首是CPU offload不是量化本身也不是模型体量。只要那一大坨权重还在内存里后面的任何细节优化都白搭。这里我还想多说一句旧版llama.cpp的问题。早期llama.cpp对V100的sm_70架构支持并不好很多CUDA kernel是为新卡优化的在V100上要么回退到通用实现要么直接跑出偏低性能。所以在V100上跑llama.cpp版本选择很关键这一点后面避坑清单里还会细说。3. 第一次提速换Q4量化把模型全部搬进显存既然瓶颈在CPU offload下一步就很明确把权重压到能整体放进16GB显存的大小。Q8_0是27GBQ4_K_M是16.3GB数值上刚好压线。这一步带来的提升是跳跃式的从4 tok/s直接干到了16-18 tok/s。3.1 量化格式怎么选Q4_K_M、Q4_0、Q3_K_MGGUF家族里4-bit附近的格式有好几个我第一次用的是Q4_K_M因为它在4-bit量化里口碑最好分组量化加关键张量保护质量损失比Q4_0小。Q4_0更轻文件约15.5GB但质量稍差。如果还想再省空间还有Q3_K_M的13.9GB。在这张16GB卡上选Q4_K_M其实有点赌运气文件16.3GB加上CUDA context、KV cache、临时激活值实际占用很容易超过16GB一旦超了一点llama.cpp就会把最后几层悄悄放回CPU。所以我后来实际部署时更倾向两条路要么用Q4_0多留一点余量要么用Q4_K_M并把上下文砍到4096以下。经过对比Q4_K_M 短上下文的效果最好质量损失小速度也不差。3.2 显存是怎么省出来的很多人忽略了一点量化省下的不只是权重占比还包括整个推理链路的带宽压力。Q4_K_M权重16.3GB理论带宽上限是900/16.3≈55 tok/s第一批优化到位后跑16-18 tok/s说明还有很大余量。这个数字看起来低但原因不是带宽不够而是llama.cpp的kernel在V100上没有完全吃满以及attention部分还在用老实现把时间浪费在显存反复读写上了。显存腾挪的具体操作包括上下文从默认8192砍到4096。Qwen2.5-27B的KV cache大约128KB/token后文会算8192上下文就要占1GB显存。砍一半就能给权重和运行时留出宝贵空间。用--n-gpu-layers 99强制所有层进GPU不让llama.cpp自作聪明。默认的显存分配策略在边界情况下经常把层踢回CPU反而导致速度雪崩。关闭不必要的--mlock以外的host侧缓冲减少显存碎片。实际的启动命令大致长这样llama-server -m Qwen2.5-27B-Instruct-Q4_K_M.gguf \ --n-gpu-layers 99 \ --ctx-size 4096 \ --parallel 1 \ --batch-size 512 \ --ubatch-size 512 \ --host 0.0.0.0 --port 8080--batch-size 512和--ubatch-size 512是给prompt prefill阶段用的。prefill时能并行处理的token数量由batch size决定batch太小的话长文档的首字延迟会非常难看。3.3 实测结果从4到18这一步之后单流生成速度稳定在16-18 tok/s。生成的文字终于能正常读下来了但离“好用”还有距离。当时我记了一笔prompt prefill速度大概在200-400 tok/sdecode速度16-18 tok/s。decode是最终用户感知最强烈的部分所以后面的优化重点都压在decode上。另外我确认了一个现象--ctx-size从4096往上加到8192时速度掉到13-14 tok/s显存占用接近100%且部分层开始出现offload。这说明Q4_K_M在这张卡上几乎占满了全部家底上下文稍微长一点就绷不住。想加回长上下文要么换更小的量化要么对KV cache动手。这两个方向后面都有对应操作。4. 后端之争llama.cpp、ExLlamaV2、vLLM在V100上的真实表现当llama.cpp跑到16-18 tok/s后我开始琢磨第二条路换一个更懂V100的推理后端。这个过程持续了将近一周试了llama.cpp的多个版本、ExLlamaV2和vLLM。结论可能对很多人有参考价值不同的后端适合不同场景在V100上差距比在新卡上更明显。后端量化格式适合场景V100兼容性llama.cppGGUF单机、轻量服务、快速部署好但kernel优化不如新卡ExLlamaV2EXL2/GPTQ单机高吞吐、极致decode性能较好需源码编译sm_70vLLMGPTQ/AWQ高并发、服务化、连续批处理版本选型敏感显存压力大4.1 为什么ExLlamaV2在V100上表现更好ExLlamaV2的定位是“单卡高吞吐”它的CUDA kernel对decode阶段的优化非常激进attention实现也预留了省显存的路径。更重要的是它在设计上不依赖BF16可以在FP16下稳定工作这正好绕开了V100不支持BF16的硬伤。同样的EXL2 4.0bpw量化模型在ExLlamaV2上跑出来比llama.cpp跑同样大小的GGUF要快30%以上。这个差异在V100上比在4090上更明显。原因是新卡的GPU算力过剩把kernel优化不到位带来的浪费掩盖掉了而V100算力和带宽都比较紧张kernel效率的差异就完全暴露出来。需要提醒的是ExLlamaV2的预编译wheel默认不一定包含sm_70的kernel所以我是从源码编译的。编译时要把TORCH_CUDA_ARCH_LIST设成7.0export TORCH_CUDA_ARCH_LIST7.0 pip install exllamav2 --no-binary :all:4.2 EXL2量化如何获得EXL2是ExLlamaV2自带的量化格式特点是支持混合bit率可以让模型的不同层按重要性分配不同的bit数。权重从HuggingFace上有人提前量化好的版本也可以自己用safetensors权重转。如果自己转需要一台显存足够大的机器或者用CPU慢慢跑python -m exllamav2.convert \ -i /models/Qwen2.5-27B-Instruct \ -o /models/Qwen2.5-27B-EXL2-4.0bpw \ -hb 4.0 \ -cf 1.0, 2.0, 3.0-hb参数是目标bit率。4.0bpw的模型文件约15.3GB3.3bpw约12.8GB。EXL2的分组和校准算法让质量在同等bit率下比GGUF Q4系列要稍好一点代价是还需要额外的校准数据集和时间。如果自己转模型记得下载一份校准数据-cf参数可以从某个校准分布里做混合bit分配。4.3 切换到ExLlamaV2之后的实测切换到ExLlamaV2 EXL2 4.0bpw、上下文4096之后单流速度从18左右跳到了25-28 tok/s。提升主要来自kernel效率和attention路径的优化。此时显存占用大约15.6GB依然处于“满负荷”状态。我再开一个上下文2048的测试时速度能到30 tok/s左右因为KV cache空间更宽裕了attention部分的压力更小。这一阶段的命令或者Python脚本都很简单直接用官方示例改路径就行from exllamav2 import ExLlamaV2Config, ExLlamaV2, ExLlamaV2Cache, ExLlamaV2Tokenizer config ExLlamaV2Config(/models/Qwen2.5-27B-EXL2-4.0bpw) config.max_seq_len 4096 model ExLlamaV2(config) model.load() cache ExLlamaV2Cache(model, max_seq_len4096) tokenizer ExLlamaV2Tokenizer(config)4.4 vLLM的尝试与放弃我也试过vLLM。它在高并发场景下的吞吐确实强但在V100 16GB上跑27B模型我遇到的实际问题比收益多新版本vLLM很多已经不把sm_70列入支持范围需要选老版本0.5.x配CUDA 11.8相对稳选型和环境折腾成本高。vLLM默认要为KV cache预留较多显存gpu_memory_utilization调到0.9以上才能装下模型一旦上下文波动大很容易OOM。单流decode速度没有比ExLlamaV2快优势只在多并发连续批处理时才能体现。最终方案是vLLM暂时不用但后续如果要做正式API服务我会考虑专门开一台显存更大的卡来跑vLLMV100这张卡留给ExLlamaV2和llama.cpp更合适。5. 细节是魔鬼KV Cache量化、FlashAttention和上下文裁剪速度到25-28 tok/s之后我停了两天重新把显存账算了一遍发现还能从三个“细小但关键”的地方挤出空间和速度KV cache量化、FlashAttention、上下文裁剪。这三个方向每一项单独看都只带来5%-15%的提升但叠在一起效果非常可观。5.1 KV cache长上下文的隐形杀手很多第一次接触大模型部署的人会忽略KV cache的存在。实际上它的大小是可以用公式算出来的。Qwen2.5-27B的KV cache每一项指标是64层、4个KV头、head_dim128、FP16下每个元素2字节。每个token的KV cache大小是2K和V两个矩阵× 64层 × 4个KV头 × 128维度 × 2字节 131,072字节也就是128KB/token。听起来不多但放到上下文长度里就吓人了上下文长度KV cache占用FP162048256MB4096512MB81921GB327684GB在16GB显存里4GB的KV cache几乎是不可接受的。所以我在阶段一就把上下文限制在4096。但即使这样512MB依然不小。此时可以对KV cache用8-bit甚至4-bit量化。这里所指的“cache量化”在llama.cpp里通过--cache-type-k和--cache-type-v设置。llama-server -m Qwen2.5-27B-Instruct-Q4_K_M.gguf \ --n-gpu-layers 99 \ --ctx-size 4096 \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ --flash-attn实测下来Q8_0的KV cache量化对质量影响很小显存占用却减半。对于上下文4096的情况KV cache从512MB降到256MB。这256MB额外空间不会直接提升速度但它能防止某个瞬间显存波动导致层被踢回CPU间接拉高稳定性。ExLlamaV2里则可以设置cache_mode为FP16或Q8类似原理。后续测试中我始终开着Q8的KV cache。5.2 FlashAttention在老卡上的真实效果FlashAttention的主要作用是减少attention计算中中间张量的显存占用和读写量。在V100上有一个特殊情况官方FlashAttention 2只支持Ampere之后的新卡V100只能走FlashAttention 1或厂商后续适配的路径。llama.cpp里的--flash-attn实测在V100上是可以工作的但提升幅度没有新卡那么夸张。我记录的对比是开与不开之间大约有5%-8%的速度差异同时显存占用下降几百MB稳定性更好。ExLlamaV2的attention实现本身已经比较接近FlashAttention的思路切换它的flash路径后提升有限但长上下文时更稳定。这里我的建议是除非你的后端明确不支持否则一定要开FlashAttention这是一个“免费的午餐”。5.3 上下文裁剪不丢人在性能调优的语境下无脑拉满ctx-size是最常见的资历错误。我的做法是先明确业务场景当前测试任务主要是代码理解和短文本问答每条输入通常不超过1500 token输出不超过800 token。那么上下文设定为2048就够用了4096都属于冗余。把上下文从4096进一步压到2048后KV cache从512MB降到256MB加上量化后实际只有128MB。这部分释放的空间让GPU可以给权重层留更多缓存余量最终实测速度从25-28 tok/s小幅提升到30-33 tok/s。速度变化不大但显存余量显著增加系统更稳了。如果你明确需要长上下文就不要走这条路而是优先选择KV cache量化和更激进的权重量化。这个决策树在部署前就要想清楚。6. 冲刺64 tok/s量化再激进一点并发再压一压到30-33 tok/s单流的优化空间已经很有限了。直接原因前面算过V100的带宽物理极限在那里摆着即便用Q3_K_M这类13.9GB的模型单流理论上限也就是900/13.9≈65 tok/s实际受kernel效率影响能到40左右已经算不错。真正把整体吞吐顶到64 tok/s的是下面两件事的组合把单流速度推到40附近再用并发把聚合吞吐拉上去。6.1 从4bpw降到3.3bpw用一点质量换速度在ExLlamaV2里我把模型的平均bit率从4.0降到3.3。重新量化后的EXL2模型文件约12.8GB这让显存压力大幅下降甚至能容纳更长一点的上下文。3.3bpw听起来损失很大但因为EXL2支持混合bit分配量化算法会把“预算”优先给重要层和敏感张量实际质量损失远比想象中可控。我在MMLU和C-Eval的样本集上抽测掉点不到1个百分点而速度从30-33 tok/s升到了38-42 tok/s。这一步还有一个附加收益显存从15.3GB降到12.8GB多出约2GB空间。这2GB我用来把上下文从2048调到4096虽然KV cache只吃256MB左右并且在显存里预留一部分buffer让系统不会因为内存碎片或瞬时峰值直接OOM。实测中3.3bpw 4096上下文 KV cache量化的组合单流稳定40上下偶尔能冲到42。6.2 单流速度的瓶颈到了物理层到这里单流速度的瓶颈基本撞上了物理层。3.3bpw模型权重约12.8GB40 tok/s意味着每秒要从HBM读取大约12.8GB×40512GB的权重再加上KV cache和激活值的读取实际内存流量已经达到600GB/s以上接近V100 900GB/s带宽的七成。kernel效率再高也很难再有质的飞跃。所以在单卡V100上27B模型的单流速度天花板大概就是40-45 tok/s这个区间。如果想继续往上走只剩两条路一是用投机解码让一个小模型先草拟多个token再让大模型验证二是走并发批处理通过多个请求复用来压满GPU带宽。我在生产环境里选择了后者因为它不需要改模型、不需要额外的草稿模型风险更小。6.3 并发批处理单流40聚合64ExLlamaV2本身支持动态批处理llama.cpp的llama-server则用--parallel参数控制并发序列数量。我先后把并发数从1调到2、4、8记录了聚合吞吐和单流延迟并发数单流平均延迟聚合吞吐130-40ms约40 tok/s260-80ms约48-52 tok/s4120-160ms约60-64 tok/s8250-400ms约58-62 tok/s开始波动从这组数据能看出两个现象并发从1提到4时聚合吞吐从40涨到64提升非常明显并发从4提到8时吞吐反而开始下降或波动。原因是并发太高时每个sequence的KV cache都在抢显存上下文一旦累积变长显存接近上限反而触发了一些层offload或显存抖动。所以在V100 16GB上27B模型的最佳并发点不是越大越好而是2-4。最终稳定使用的服务化配置是# ExLlamaV2中启用并发 cache ExLlamaV2Cache(model, max_seq_len4096, lazy_modeTrue) model.set_measure_mode()或者直接用llama.cpp的--parallel 4。实测两种方案的聚合吞吐都在60-64 tok/s附近。最终选择哪种取决于你已有的量化格式手上如果已有在用的GGUF用llama.cpp更省事如果愿意为多30%的单流性能换EXL2就上ExLlamaV2。需要特别说明64 tok/s是聚合吞吐不是单流返回速度。对最终用户来说单个请求的响应速度还是30-40 tok/s的体感。不过对于API服务的总体容量、成本核算和批量任务处理64的聚合吞吐意味着同样的QPS可以支撑更多用户这是服务化场景更看重的指标。7. V100上跑27B最容易踩的坑我替你们踩完了整个调优过程前后折腾了两周多踩过的坑比预想的多。我总结成一份避坑清单按严重程度排序列出来每一条都是真实经历过或者通过社区案例验证过的。第一编译llama.cpp时没有指定sm_70架构。默认构建可能只包含当前机器GPU的架构或最新代架构放到V100上会出现no kernel image available错误或者某些kernel走到了兼容性很差的通用实现。务必加-DCMAKE_CUDA_ARCHITECTURES70再编译。cmake -B build -DGGML_CUDAON -DCMAKE_CUDA_ARCHITECTURES70 cmake --build build --config Release -j $(nproc)第二忽略V100不支持BF16的问题。很多新模型权重和第三方转换工具默认导出BF16直接在V100上跑会报不支持或产生错误结果。新版llama.cpp会自动处理一部分但ExLlamaV2之类的老后端可能要把权重转换为FP16再加载。第三盲目信任模型的默认上下文长度。现代模型动辄32k上下文花在KV cache上的显存不可忽略16GB卡上更是直接爆显存。部署前先按照第5.1节的公式算一下KV cache占用再决定ctx-size。第四新版本vLLM不再支持V100。如果你坚持要用vLLM请锁定0.5.x CUDA 11.8的组合新版本很可能在安装阶段就报架构不支持。这个坑排查起来很费时间因为报错信息不一定直接指向架构问题。第五量化格式不是越“高级”越好。GPTQ和AWQ在新卡上有专门的硬件加速路径但V100的Tensor Core对INT4支持弱实际跑起来不一定比GGUF Q4_K_M或EXL2快。在没有实测数据之前不要凭“大家都在用GPTQ”来判断。第六并发数不是越高越好。我在第6.3节的数据说明了一切。V100显存小并发过高反而导致KV cache膨胀、上下文挤压部分层被迫offload到CPU速度雪崩。先从小并发测起观察显存占用和速度拐点。第七注意温度和功耗墙。V100满载功耗250-300W机房散热不到位时长时间跑并发很容易被降频。我遇到过满载十分钟后速度悄悄掉回30 tok/s的情况用nvidia-smi一查是核心温度到了85度触发降频。后来调整了机柜风道温度控制在75度以下速度才稳定住。第八量化文件版本要对齐。同样是Qwen2.5-27B不同来源的GGUF文件可能因为imatrix校准数据集不同产生不同的质量。想让模型更聪明建议找到带高质量imatrix的版本或自己跑一遍校准。质量不是单纯看bit率。第九监控要一直开着。部署过程中千万不要只盯着最终速度。用nvidia-smi dmon或nvtop持续观察显存、功耗、温度用llama-server --verbose看每一层的耗时分布才能快速定位瓶颈到底是CPU、GPU还是带宽。很多莫名其妙的问题监控一开就原形毕露了。8. 最后的个人体会回头再看这轮调优最值钱的经验不是某个具体参数而是一套判断思路先算物理上限再找瓶颈最后才是调参数。第一次上手就想着“换个好用的量化格式”就能变快结果4 tok/s教我做人了。真正提速的关键是把模型完整放进显存、选对后端、管好KV cache、在单流极限之上用并发换吞吐。如果让我重新来一次我会直接跳过后面的弯弯绕绕照着这个顺序走先拿Q4_K_M或EXL2 3.5bpw量化好模型、确认所有层都在GPU上、上下文按需裁剪、开KV cache量化和FlashAttention、用ExLlamaV2或新版本llama.cpp做单流测试等单流到40 tok/s左右再上2-4路并发把聚合吞吐顶到60以上。再分享一个小技巧调优过程中把每一步的配置和实测数据记录下来包括量化格式、上下文、并发数、GPU温度、显存余量。很多“玄学”性能波动回头看都是温度、显存碎片、版本差异造成的。有了记录复现和排障都会轻松得多。毕竟在V100这种老卡上压榨性能靠的不是一个魔法参数而是一连串扎实的小优化叠出来的结果。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →