同一台笔记本,同一个模型文件,命令行只差一个参数。结果是显卡把解码速度拉到了CPU的4.3倍。
这不是服务器机房的对比,是一台13代酷睿i7-1360P笔记本,机箱里还塞着一块GTX 1650 Ti Max-Q,显存只有4GB。测试跑的是同一个量化后的Gemma 4小模型,两边喂进去的字节完全一致。
两台"机器",只差一个开关
测试的设计思路很直接:一台机器,两条路径。一条只用CPU,一条把模型层卸载到那块4GB显卡上。除此之外,所有条件对齐。
命令行里唯一变动的,是-ngl这个参数——0代表全部留在CPU,99代表尽量往显卡上放。其余参数一字未改:上下文8192,KV缓存用f16,开启flash attention,线程数固定。
模型是3.35GB的量化感知GGUF文件。两条路径交替运行,中间强制冷却120秒,避免温度干扰结果。
八组数据,解码快4.27倍
测试矩阵是四档提示长度乘以两档输出长度,共八个格子,每格重复三次,并发数为1。解码速度取的是客户端从SSE流里测出的token间隔速率——这是两条路径都能产出的唯一解码指标。
八格中位数结果:
- 解码速度:4.27倍
- 预填充速度:3.63倍
- 端到端耗时:3.81倍
这个差距是不是噪声?三次重复的组内波动,CPU那边最差6.29%,显卡这边只有0.84%。解码倍率在所有格子里落在4.04倍到4.34倍之间。效应量比噪声高出一个数量级,这才值得拿出来说。
解码几乎不动,预填充线性增长
把解码那两列竖着看,而不是横着比。
CPU解码从17.61 tok/s滑到15.66 tok/s,显卡解码从71.22滑到67.61。提示长度跨度达到21倍,两边的解码速度几乎纹丝不动。
但首token延迟完全是另一副面孔:CPU从1143毫秒涨到23926毫秒,显卡从385毫秒涨到6522毫秒——随提示长度线性上升。
这是同一个规律被说了两遍。解码每生成一个token都要把整个模型读一遍,瓶颈在显存带宽;预填充要把提示词逐层乘过去,瓶颈在算力。加速卡对两者都有帮助,但帮助的原因不同。只报一个数字的跑分,恰好把这件事藏起来了。
解码倍率还会随上下文变长而爬升:94个token时是4.04倍,998个token时是4.34倍。KV缓存越长,CPU这边掉得越多,显卡这边几乎不动。
3.35GB怎么塞进4GB显存
答案是:根本不用全塞进去。
模型加载并开始服务后,显卡上实际只占了1598 MiB。
那个3.35GB的文件里,Q4_0量化只占32%。两个嵌入张量都是Q6_K格式,在3.334GB的张量字节里占了2.257GB。其中最大的那个叫per_layer_token_embd,单个就有1.93GB,占整个文件的58%。
它在llama.cpp的src/models/gemma4.cpp里用TENSOR_READ_LAZY创建,由GGML_OP_GET_ROWS直接从内存映射里读取。每个token只碰其中几行,这部分数据从头到尾就没上过显卡。
真正常驻显卡的,是约1.08GB的Q4_0 transformer主体——这是解码每个token都要读的部分,再加上约60MiB的KV缓存和计算缓冲区。
同一套机制在CPU路径上不是省了资源,而是付出了代价。匿名驻留内存(RssAnon)这一项,就是代价的落点。
一个参数背后的取舍
对开发者来说,这件事的可操作部分很小:一块四年前的中端笔记本显卡,一个-ngl参数,就能把本地小模型的解码速度抬到四倍以上。
但前提是理解那个"没上显卡的58%"——模型文件大小不等于显存占用,量化格式的分布决定了哪些张量必须常驻、哪些可以懒加载。把这两件事混为一谈,就会得出"4GB显卡跑不了3.35GB模型"的错误结论。
测试仓库已经公开在GitHub上,路径是xbill9/gemma4-dev。所有原始数据、命令行和冷却策略都在里面,可以自己复现。
热门跟贴