不用看图表,不用看天花乱坠的“一键部署”,这篇就是实打实的记录:怎么把一块老旧的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_70,16GB HBM2显存。刚拿到手时我也觉得它“过时”,但把参数表摊开仔细看,它并没有想象中那么不堪。重点是显存带宽:约900GB/s。作为参照,RTX 4090大概是1008GB/s,A100 40GB是1555GB/s。V100的带宽是4090的九成,放在今天依然够看。
大模型解码阶段的核心矛盾,在于“把权重从显存里读出来”这件事,而不是“计算”本身。生成一个token,理论上需要把整个模型的权重从HBM过一遍,在batch很小的时候,计算单元经常是闲着等数据的。我做了一个粗略估算:27B模型做4-bit量化后权重大约15-16GB,V100的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层Transformer,hidden_size=3584,28个Q头,4个KV头(GQA),head_dim=128。这里埋伏了一个对部署很友好的点:GQA让KV头数量只有4个,KV cache体量不大,在显存紧张时很有价值。
模型权重在不同精度下的体量大概如下表:
| 精度/量化格式 | 文件大小(约) | 是否能塞进V100 16GB |
|---|---|---|
| BF16 | 55GB | 否 |
| Q8_0 | 27GB | 否 |
| Q6_K | 21GB | 否 |
| Q5_K_M | 18GB | 否 |
| Q4_K_M | 16.3GB | 勉强(KV cache空间很少) |
| Q4_0 | 15.5GB | 勉强 |
| Q3_K_M | 13.9GB | 是(空间较宽裕) |
| EXL2 4.0bpw | 约15.3GB | 勉强 |
| EXL2 3.3bpw | 约12.8GB | 是 |
Qwen2.5-27B这一代模型在BF16下权重约55GB,8-bit约27GB,6-bit约21GB,单卡16GB的V100想都不用想,必须上4-bit甚至更小。所以我从一开始就把这次任务定性为:一个显存预算问题,而不是算力问题。所有调优动作,本质上都是在“把模型完整塞进显存”和“给KV cache留出空间”之间找平衡。
2. 那个令人绝望的4 tok/s是怎么来的
4 tok/s不是某一步偶然踩出来的,它是一套“看起来合理,实则处处撞墙”的默认配置的必然结果。我第一次部署时用的组合是:llama.cpp的一个偏旧版本 + Q8_0量化GGUF + 默认上下文8192 + 让llama.cpp自动决定GPU层数。这套配置的结果是:模型约27GB,GPU只能装下不到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是27GB,Q4_K_M是16.3GB,数值上刚好压线。这一步带来的提升是跳跃式的,从4 tok/s直接干到了16-18 tok/s。
3.1 量化格式怎么选:Q4_K_M、Q4_0、Q3_K_M
GGUF家族里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/s,decode速度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.cpp | GGUF | 单机、轻量服务、快速部署 | 好,但kernel优化不如新卡 |
| ExLlamaV2 | EXL2/GPTQ | 单机高吞吐、极致decode性能 | 较好,需源码编译sm_70 |
| vLLM | GPTQ/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.0:
export TORCH_CUDA_ARCH_LIST="7.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.3GB,3.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_len=4096) 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服务,我会考虑专门开一台显存更大的卡来跑vLLM,V100这张卡留给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_dim=128、FP16下每个元素2字节。每个token的KV cache大小是:
2(K和V两个矩阵)× 64层 × 4个KV头 × 128维度 × 2字节 = 131,072字节,也就是128KB/token。
听起来不多,但放到上下文长度里就吓人了:
| 上下文长度 | KV cache占用(FP16) |
|---|---|
| 2048 | 256MB |
| 4096 | 512MB |
| 8192 | 1GB |
| 32768 | 4GB |
在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.8GB,40 tok/s意味着每秒要从HBM读取大约12.8GB×40=512GB的权重,再加上KV cache和激活值的读取,实际内存流量已经达到600GB/s以上,接近V100 900GB/s带宽的七成。kernel效率再高,也很难再有质的飞跃。所以在单卡V100上,27B模型的单流速度天花板大概就是40-45 tok/s这个区间。
如果想继续往上走,只剩两条路:一是用投机解码,让一个小模型先草拟多个token再让大模型验证;二是走并发批处理,通过多个请求复用来压满GPU带宽。我在生产环境里选择了后者,因为它不需要改模型、不需要额外的草稿模型,风险更小。
6.3 并发批处理:单流40,聚合64
ExLlamaV2本身支持动态批处理,llama.cpp的llama-server则用--parallel参数控制并发序列数量。我先后把并发数从1调到2、4、8,记录了聚合吞吐和单流延迟:
| 并发数 | 单流平均延迟 | 聚合吞吐 |
|---|---|---|
| 1 | 30-40ms | 约40 tok/s |
| 2 | 60-80ms | 约48-52 tok/s |
| 4 | 120-160ms | 约60-64 tok/s |
| 8 | 250-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_len=4096, lazy_mode=True) 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_ARCHITECTURES=70再编译。
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=70 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这种老卡上压榨性能,靠的不是一个魔法参数,而是一连串扎实的小优化叠出来的结果。