[控场AI]
· 8 分钟阅读· 4,152 字

llama.cpp MoE缓存实测:T4上30B模型解码速度翻倍

llama.cpp MoE缓存实测:T4上30B模型解码速度翻倍

llama.cpp 新 MoE 缓存让 T4 上 30B 模型解码从 22.6 提升到 43.2 tokens/s。

llama.cpp 从 build b11480 起引入 MoE 专家缓存,把跨层高频选中的专家留在 VRAM,未命中的才经 PCIe 上传,淘汰策略为 LRU。在 16GB Tesla T4 上运行 18.6GB 的 Qwen3-30B-A3B,配合 --cpu-moe 和 10,000 MiB 缓存,解码速度从默认 22.6 tokens/s 提升到 43.2 tokens/s,命中率 95.7%。缓存只覆盖 ffn_*_exps 张量,且仅服务 32 token 以内批次。文章强调预算接近下限比不设缓存更糟,并发和投机解码会放大批次、加剧淘汰,缓存开启后输出也不再逐位一致,适合单人或小团队场景。

llama.cpp MoE缓存实测:T4上30B模型解码速度翻倍

大模型跑在显存不够的GPU上,通常只能把部分层放CPU,然后苦等最慢的一环。llama.cpp 从 build b11480 开始提供了一个新思路:与其把整层专家塞进显存,不如用一块缓存,把模型实际高频选中的专家留在VRAM里。

我们用一张免费的16GB Tesla T4跑Qwen3-30B-A3B(18.6GB,装不下),解码速度从默认的22.6 tokens/s提升到43.2 tokens/s——接近翻倍。这个功能已在10月合并进主分支,多卡支持随后跟上。

llama.cpp MoE缓存实测

MoE缓存到底缓存了什么

llama.cpp 的做法很直接:把专家全在内存里的那些层挑出来,按专家张量的类型和形状分组,每组在VRAM里分一个槽位池。路由器选中专家时,命中的直接从显存读,未命中的才走PCIe上传,淘汰策略是LRU。池子是共享的,所以第3层的热门专家可以挤掉第40层的冷门专家。

需要注意,缓存只覆盖 ffn_*_exps 张量,路由器、注意力和共享专家都不在其中。另一个关键限制是批大小:缓存只服务32个token以内的批次,更大的批次(也就是prompt处理)走旧的卸载路径,但会把已缓存的专家直接拷到GPU上,而不是重新上传。

专家张量的命名与分组逻辑:llama.cpp 内部用 ffn_*_exps 这一命名模式来标识 MoE 层中的专家权重张量,常见的具体名称包括 ffn_gate_exps、ffn_up_exps 和 ffn_down_exps,分别对应每个专家的门控、升维和降维投影。缓存机制会扫描模型的所有层,把包含这些张量的层单独拎出来,再按张量的数据类型(如 Q4_K、Q6_K)和形状(专家数 × 单专家维度)做分组。同一组内的专家共享一个 VRAM 槽位池,槽位大小严格等于一个专家张量在该类型下的字节数。正因为分组依据是“类型+形状”,Q4_K_M 这种混合量化格式会把专家拆成不同组,这也是后文 Qwen3-30B-A3B 被分成两个 24 层组的原因。理解这一点,才能看懂脚本算出的槽位数为何与启动日志一致。

配置之前,先算清楚你的专家有多大

拍脑袋设一个 --moe-cache-mib 很容易踩坑。原文提供了一个纯标准库的Python脚本,直接读GGUF头,复刻llama.cpp的槽位计算逻辑,让你在加载模型前就知道预算能换多少槽位。

对Qwen3-30B-A3B Q4_K_M来说,专家总量16.35 GiB,48层、128个专家、每token用8个,因为Q4_K_M混合量化,这些专家被分成两个24层的组。设10,000 MiB,每组拿到1,834个槽位,占专家的59.7%——和T4上llama-server启动日志报出的数字完全一致。

这里有个硬下限:低于某个预算,llama.cpp会直接放弃缓存该组,只给一条警告;如果所有组都不达标,服务器根本起不来。实测中,50 MiB是底线,49 MiB就会报错退出。这一点值得警惕,预算接近下限比不设缓存更糟——槽位太少会被快速淘汰,反而拖慢速度。

GGUF 头解析与槽位预算公式:GGUF 是 llama.cpp 使用的模型文件格式,头部以键值对形式记录了张量名称、形状、数据类型和偏移量,无需加载权重即可读取。原文脚本正是利用这一特性,用纯标准库解析头部,定位所有 ffn_*_exps 张量,按类型和形状分组,再用 预算字节数 ÷ 单专家字节数 ÷ 组内层数 反推每组能分到多少槽位。这里的关键是“组内层数”参与除法:因为槽位池是组内共享的,预算要平摊到该组的每一层,所以同样的 MiB 预算,层数越多的组每层分到的槽位越少。脚本还会输出槽位占专家总数的百分比,帮助你在加载前判断预算是否接近硬下限,避免出现“50 MiB 能跑、49 MiB 报错”的尴尬。

两种跑法:自动放置 vs 手动接管

第一种是省事的跑法:保留 --fit 自动放置,它会考虑缓存占用的显存。在T4上,--fit 主动少放了1,670 MiB的模型层,腾出空间给1,671 MiB的缓存。

第二种是更快的跑法,也是原文推荐的:用 --cpu-moe 把所有专家放到内存,把腾出的显存全给缓存。原因很直白:一整层在VRAM里,只有路由器选中该层某个专家时才有用;而缓存装的是跨层的高频专家,命中率更高。

不过手动接管意味着你要自己算显存账。--cpu-moe 是张量覆盖,一旦设置,--fit 就不再挪层,如果放不下只会给警告然后失败。记得给稠密权重、KV缓存和计算缓冲区留足空间。

--fit 与 --cpu-moe 的语义差异:--fit 是 llama.cpp 的自动显存放置策略,它会在启动时探测可用 VRAM,按优先级把模型层分配到 GPU 或 CPU,并预留 KV 缓存和计算缓冲区;开启 MoE 缓存后,它会把缓存占用也算进预算,主动少放若干层来腾空间。而 --cpu-moe 是一个张量覆盖开关,语义是“所有 MoE 专家张量一律放 CPU”,它优先级高于 --fit 的自动决策,一旦设置,--fit 不再移动这些专家层,剩余显存全部留给稠密权重、KV 缓存和专家缓存。两者的根本区别在于:--fit 是“层粒度”的粗放放置,一层要么全在 GPU 要么全在 CPU;--cpu-moe 则把专家彻底移出 GPU,再用缓存以“专家粒度”按需调入,命中率天然更高。这也是原文推荐手动接管的原因。

不看日志,你根本不知道命中率

缓存的大小和命中率默认是看不到的。它们是libllama的INFO级日志,被llama-server归到verbosity 4,高于默认阈值3。加上 -lv 4 才能在启动时看到缓存大小,在停止服务后看到命中率统计。

T4实测的命中率日志显示:8 token以内的小批次命中率95.73%,9到32 token的批次命中率59.91%。大批次虽然不用槽位,但会从缓存里拷贝了46,881个专家,节省了大量上传。

T4上的真实成绩单

原文给出的对比数据(Qwen3-30B-A3B Q4_K_M,单卡T4)很能说明问题:

  • --fit 默认:22.6 tokens/s
  • --fit + 10%缓存:23.6 tokens/s,命中率75.6%
  • 仅 --cpu-moe:9.6 tokens/s
  • --cpu-moe + 50 MiB缓存:3.8 tokens/s(反面教材)
  • --cpu-moe + 10,000 MiB缓存:43.2 tokens/s,命中率95.7%

你可能没注意到,--fit 下10%的缓存虽然命中率不低,但速度提升有限,因为它只覆盖了被留在内存的20层,而且为了腾空间牺牲了常驻层。而 --cpu-moe 配上大缓存,让95.7%的专家查询命中显存,才真正释放了性能。prompt处理速度也从150提升到382 tokens/s。

作者在RTX 5090上还测到67.8 tokens/s(19,000 MiB缓存,89%命中率),说明更大的缓存仍有收益。不过作者也提醒,这只是单模型、单提示集、单主机的数据,10%只是一个开始测量的起点。

什么时候缓存会帮倒忙

并发会吃掉缓存。 llama-server默认4个并发槽位,四个用户同时生成意味着每步要处理4个token、各自选专家,槽位需求翻四倍,LRU淘汰更频繁。T4上四请求并发总吞吐37.0 tokens/s,还不如单请求,命中率也从95.7%降到92.3%。这个功能适合单人使用或小团队,不适合繁忙的在线服务。

投机解码同理。 验证草稿会一次性喂入多个token,放大批次,而草稿模型本身没有缓存。

多卡有额外限制。 预算按tensor-split或空闲显存分配,缓存开启时会关闭流水线并行,张量并行模式直接拒绝启动。

输出不再逐位一致。 缓存让专家在CUDA内核里运行而不是CPU,结果不再逐位相同,温度0下的贪心选择可能翻转。原文实测中,--cpu-moe 下4条回复只有1条与无缓存版本逐字一致,其余在15到367个字符处开始分叉。质量评估还是得用你自己的评测集。

LRU 淘汰与批大小放大的联动效应:LRU(最近最少使用)淘汰策略在缓存满时总是踢掉最久未被访问的专家。单请求小批次下,每个 token 只选 8 个专家,访问模式集中,热门专家反复命中,淘汰压力小;但并发或投机解码会在一 步内产生多个 token、各自选专家,瞬时需要的槽位数成倍增长,缓存被迫频繁换入换出,PCIe 上传次数反而上升。更隐蔽的是,被淘汰的专家可能在下一次迭代立刻又被需要,形成“抖动”,此时缓存不仅不省带宽,还额外增加了管理开销。这解释了为何 T4 上四请求并发总吞吐反而不如单请求,也说明评估缓存收益时必须用真实并发数回放,而不能只看单流命中率。

上手前先做这四件事

  1. 用原文脚本跑一遍你的GGUF,确认 --moe-cache-mib 能换到多少槽位。
  2. 加 -lv 4 启动,核对启动日志的槽位数,确认没有“预算太小”的警告。
  3. 用真实提示和实际并发数回放,停止服务后读 ubatch <= 8 的命中率。
  4. 和 --moe-cache-mib 0 的同一回放对比解码速度。

如果你的MoE模型装不进单卡,升级到b11712或更高版本,跑一下脚本,用 --cpu-moe 加上大部分空闲显存作为缓存,再开启 -lv 4。在T4上,这只是一个参数加一次重启,就换来了接近两倍的速度。

#llama.cpp#MoE缓存#专家缓存#Tesla T4#Qwen3-30B-A3B#本地推理优化#GPU显存优化
分享:

相关推荐