开源llama.cpp Releases·原文 2026年9月15日

llama.cpp b10981发布:OpenVINO后端优化有状态解码与GPU MoE推理

该版本修复了Gemma-4各层注意力头数不一致导致的状态化解码错误,并让KV状态重排对多头模型生效,在深度 8192 时gemma-4-12B从 6.27 t/s提升到 9.11 t/s。

AI解读:这次更新集中在llama.cpp的OpenVINO后端上,核心改了两件事:一是让有状态解码(stateful decode)在Gemma-4这类层与层之间注意力头数不同的模型上不再输出乱码,二是把KV状态重排的优化从单头模型扩展到多头模型。后者在GPU上实测有明显提速,而且是深度越大收益越明显。

之前的问题出在一个假设上:代码认为所有层的KV头数一样,于是用了模型级的标量。但Gemma-4的 12B每层头数并不相同:8×256 的滑动窗口层加 1×512 的全注意力层,48 层里有 40 层被按 1×2048 切分,注意力读到的状态切分是错的,CPU和GPU上都解码出乱码。31B也有类似情况(16×256 加 4×512)。现在改成按层记录头数。

另一个实用修复是拒绝而不是硬跑:当解码位置超出滑动窗口、ggml缓存不再是简单前缀时,从缓存播种KV状态会拿到错数据,过去会抛出没有说明的ov::Exception(llama_decode返回 -3)。现在这两种情况都会给出清晰报错,编译路径上同样拒绝。

性能上,pass::KVStateSeqAxis原先只对单KV头生效,理由是实测多头没有收益,但那次测量是在深度 0 做的,而这个改动在深度 0 本来就不起作用。放开后,它去掉了每生成一个token就对整个累积状态做转置的开销,改成只转置新增的一行。GPU上tg128:gemma-4-12B在深度 8192 从 6.27 提升到 9.11 t/s,Llama-3.2-1B从 47.8 提升到 59.6 t/s。

如果你是OpenVINO后端的用户,且用Gemma-4 12B/31B做长上下文推理,这次更新直接关系到输出是否正确以及解码速度,建议升级。其他后端和E2B模型不受头数问题影响——E2B每层头数都是 1。除此之外,该版本还包含MoE专家块在GPU上融合进MOECompressed、新增GGML_OPENVINO_REQUANT_KQUANT与GGML_OPENVINO_SPILL_DIR环境变量等改动。

llama.cpp发布b10981版本,主提交为OpenVINO后端的优化与修复,标题为「optimize stateful decode and GPU MoE inference (#28638)」。

其中一项修复针对Gemma-4每层注意力头数不一致的问题:有状态路径把ggml的KV缓冲区 [1, 1, seq, n_heads_kv * head_size] 重解释为 [1, seq, n_heads_kv, head_size] 时,头大小取自张量自身合并维度(因为gemma-4按层类型变化),但头数仍来自模型级标量,该标量会被compute_llm_params() 按注意力节点覆盖,最终保留的是最后一层的值。

据提交说明,gemma-4的头数同样按层变化:12B为 8×256 滑动层加 1×512 全注意力层,31B为 16×256 加 4×512。结果是 12B的 48 层中有 40 层被按 1×2048 而非 8×256 切分,注意力以错误的头切分读取状态,两个模型在CPU和GPU上都解码出乱码。E2B不受影响,其头数处处为 1。

修复方式是按层记录头数,并通过cache_k_l叶子名称查找;键按层而非层类型,因为滑动/全注意力的分类来自缓存范围,在小 -c下会打平,而头数不会。状态化状态的序列轴裁剪现在也按状态推导。

另一项修复是拒绝无法从KV状态恢复的有状态解码。有状态路径在解码位置领先于状态所持位置时,会从ggml缓存播种KV状态;这仅在ggml缓存是简单前缀、单元i持有位置i时成立。滑动窗口层只保留最后n_swa个位置并丢弃其余部分,因此超出窗口后单元i不再持有位置i,播种的状态就是错的。同时状态裁剪到解码位置没有边界检查,超出末尾会以ROI构造函数的裸ov::Exception呈现(llama_decode返回 -3,默认详细度下不给出原因)。现在这两种情况都会以清晰消息拒绝,编译路径上同样拒绝,因为新模型从空状态开始只能从头服务序列。可用llama-bench -d复现,该参数会恢复保存的序列状态而非重算深度预填充。

KV状态重排放开到多头模型,深度 8192 下gemma-4-12B从 6.27 到 9.11 t/s

pass::KVStateSeqAxis原先仅限单KV头的状态,此时把序列轴从维度 1 移到维度 2 是纯元数据变更。提交说明称该限制还基于一次测量,显示多头模型没有收益,但那次测量是在深度 0 做的,而深度 0 恰恰是这项改动不起作用的那一个深度。

多头情况下该pass不只移动元数据:它去掉了读者侧对整个累积状态的转置,否则图会在每个token上重做这一操作,代价随上下文长度增长,取而代之的是只转置新增的单行。GPU上tg128、交替对照测得:gemma-4-12B在深度 8192 从 6.27 提升到 9.11 t/s(无状态为 7.69,因此有状态现在在深度上占优而非落后),Llama-3.2-1B从 47.8 提升到 59.6 t/s。两者在深度 0 都处于噪声范围内,这也是早先检查看不到变化的原因。

状态重填现在需要复制行而非重解释:ggml存储为 [seq][n_heads_kv * head_size],而多头重排状态是另一种元素顺序。否则重填会播种错误数据,该路径目前可通过llama-bench -d触达。

GPU MoE、环境变量与其他OpenVINO改动

提交信息还列出多项改动:在GPU上把MoE专家块融合进MOECompressed;跳过未绑定专家张量的GPU MUL_MAT_ID;在GPU上对分组 8 位MoE专家重新量化;移除mul_mat_id回退,仅对mxfp4限制大型mul_mat_id。

新增两个环境变量:GGML_OPENVINO_REQUANT_KQUANT用于选择 4 位重新量化目标,GGML_OPENVINO_SPILL_DIR用于将权重缓冲区溢写到磁盘。

其它修复包括:translate_add将不匹配的操作数类型(如融合ADD_ADD中的f16/f32)上转至f32相加后一次性转回输出类型;translate_glu_swiglu_clamp同样处理,此前f16 Swish/Clamp舍入会漂移出测试容差。supports_op拒绝ne[3] > 1 的ROPE(多序列,cos/sin表只覆盖单序列),并因OpenVINO GPU内核对大输入溢出到inf而在GPU上拒绝SOFTPLUS(CPU正常)。ci/run.sh在OpenVINO GPU上串行执行test-backend-ops,因为两个worker并发会以CL_OUT_OF_RESOURCES使GPU插件崩溃。

MoE专家平面求和ADD链的ReduceSum捷径在超过 8 个专家时会漂移出 1e-7测试容差(f32累加顺序与CPU参考不同),表现为间歇性,类似既有的Q4_K/Q5_K的NMSE情况;新增is_moe_expert_sum_add() 以便supports_op按专家数限制,仅对该归约操作回退到CPU。GPU上退化的m=1,n=1 MUL_MAT也被限制:CI对一个标量输出f32点积(m=1,n=1,k=2048)测得ERR=1.8e-3(容差 5e-4),本地 8 次未复现,推测是GPU插件为该极小形状选择的内部fp16累加路径;m=1 输出维度不出现在真实模型权重中。SoftPlus分解改为可选的原生方式。

NPU无缓存编码器模型与规范优化

mmBERT使用的packed QKV视图被ROPE支持检查拒绝,导致Q/K RoPE被拆到CPU、无法检测无缓存注意力,并把碎片化的编码器图送入面向解码器的NPUW路径。现在接受packed QKV RoPE视图,从其掩码检测无缓存注意力,并以单次全序列预填充运行这些模型,不使用NPUW或解码图;同时提供静态掩码、输出索引和均值池化形状与输入。

规范与RoPE翻译被优化:用opset6 MVN操作替换分解的均值/方差归一化图,保留GGML的epsilon位置,同时让OpenVINO插件将归一化编译为单个操作、减少中间张量。RoPE正弦与余弦输出缓存在图级张量映射中,缓存键由所有RoPE参数和可选频率因子输入构成,使兼容的Q/K与层节点共享同一子图而不混用不同RoPE配置。同时暴露NodeContext::put_shared() 以发布翻译器创建的输出供图级复用。

其他整理包括:简化操作翻译器并启用IMROPE/NEOX RoPE融合;移除不必要的include与清理PAD;修复mulmat缺陷;用ov::as_type_ptr替代std::dynamic_pointer_cast;按上下文共享已编译模型并修复线程安全;针对交错SWA模型按结构分类滑动窗口层;修复Gemma-4按层类型头大小的有状态解码;修复permute中的MSVC收窄错误;排除GPU/NPU上失败的POOL_2D用例并修复pool用例。

信息来源

llama.cpp Releases原始来源