llama.cpp 于 2026 年 9 月 26 日合并 PR 27851:CPU 矩阵乘改用 256×256 int8 分块与 16×16 微内核,8192 立方矩阵下 Q3_K 预填充吞吐从 0.733 TF 升到 5.164 TF 即 7.04 倍,但 ARM 平台与纯 GEMV 解码反而变慢
llama.cpp 于 2026 年 9 月 26 日合并 PR 27851,CPU 矩阵乘改用 256×256 分块,Q3_K 吞吐升 7.04 倍。
领域锚定
前置部署 Forward Deployed Engineer(FDE)需要判断的不是跑分本身,而是这条改动落在什么硬件、什么负载上,因为企业内网与隔离环境里最常见的推理节点就是只有 CPU 的服务器。
前置部署 FDE 在客户现场经常要把模型塞进没有 GPU 的机器:机架里只有 CPU,或者 GPU 被训练队列独占,此时 llama.cpp 的 CPU 后端往往就是唯一可选项。
这次改动的收益集中在一次算很多 token 的负载,也就是预填充阶段,而企业 RAG enterprise RAG 建索引、LLM 评估 LLM evaluation 跑批、批量文档抽取恰好都属于这一类。
边界同样重要:逐 token 解码的 GEMV 场景反而更慢,ARM 平台完全不生效,所以选型之前必须先确认目标机器的指令集与负载形态。
Forward Deployed Engineer 的日常就是把上游发布的性能特性翻译成这台客户机器上跑不跑得动,本条提供了可核实的数字与一个明确的环境变量开关。
这次改动是什么
llama.cpp 的构建号 b11195 于 2026 年 9 月 26 日 08:29 UTC 发布,它是第一个带上 PR 27851 的二进制版本。
PR 27851 的标题是 ggml-cpu: tiled mul_mat for k-quants,作者是 jbooth,由维护者 ggerganov 于 2026 年 9 月 26 日合并,共 40 个提交,协作署名里包括 Bartowski。
改动全部落在 ggml 的 CPU 后端,涉及 11 个文件,新增 3050 行、删除 1326 行。
被删除的是旧的 iqp 路径:ggml/src/ggml-cpu/iqp.cpp 删掉 1253 行、同目录 iqp.h 删掉 39 行,也就是早期的 int8 量化流水线被整体移除,没有保留为可选后端。
新增的是 ggml/src/ggml-cpu/tiled/ 目录,其中 tiled.cpp 1245 行、tiled-kernel.cpp 719 行、tiled-kernel.h 180 行、tiled.h 31 行,合计约 2175 行。
测试新增 tests/test-tiled-mulmat.cpp 共 852 行,并注册进构建目标 test-tiled-mulmat。
改动前的实现与根因
改动前的 CPU 矩阵乘走 vec_dot 路线:每次内积调用都要把量化块重新解包一遍,同一份权重的解包工作被重复执行了很多次。
作者在 PR 描述里的原话是 vec_dot 路线把解包工作重复了一堆次(duplicates the work of unpacking quants a bunch of times),这就是本轮优化的根因。
解包出来的中间量并不是最终结果,只是后续乘加需要的输入,所以重复解包等于纯浪费内存带宽与指令周期。
新实现把这件事反过来做:用 256×256 的 int8 窗口沿源张量滑动,一次解包一整块,解包成本相对基线被摊薄 256 倍。
窗口内部再用一个优化过的 16×16 微内核扫过,累加出一个 256×256 的 float32 输出瓦片,最后整体写回目标张量。
如果单个窗口对机器上的核心数来说活儿太少,实现会把分块尺寸逐级降到 128、64,让并行度重新饱和,这一点在 PR 描述里被明确写出。
前置部署 FDE 从这个根因能拿走一条通用判据:当热路径里出现同一份只读数据被反复转换的模式,优先考虑放大工作窗口让转换被摊薄,而不是去优化转换本身。
新实现里的关键常量
关键常量写死在 ggml/src/ggml-cpu/tiled/tiled-kernel.h 里:TILED_TILE_K 为 256,等于一个 QK_K 块;TILED_TILE_ROWS 为 256;TILED_MICRO 为 16;每线程工作区槽位 TILED_WS_SLOT 为 512 KB。
内存里的工作区由 ggml_tiled_wdata_size 申请,公式是 64 字节对齐余量加上线程数乘以 512 KB,所以在 8 线程机器上这条路径会额外占用约 4 MB 的工作区。
另一个实现细节是 narrow 分支:当本次乘法的激活行数不超过 16 时,权重按每行长流的形态读取,k_extent 由 31 KB 的 L1 预算反算,并向下取整到 256 的倍数,目的是用连续读提升带宽。
MoE 场景单独走 MUL_MAT_ID 分支,按专家分组,组大小 TILED_MMID_GROUP 为 64,用来在单个专家被路由到的行数很小时仍然让所有线程有活干。
writeback 用了 16×8 的分块转置再写回,目的是把 j 方向连续的累加缓冲转成目标张量需要的 i 方向连续布局。
实测数字:预填充 3 到 7 倍
作者给出的整体结论是预填充场景 3 到 7 倍,相对早先的 repack 方案约 2 倍。
8192×8192 乘 8192×8192 的方阵基准(8 线程、AMD 9950x3D、5 次取最小值)里,q3_K 从 0.733 TF 升到 5.164 TF,合 7.04 倍。
同一张表里 q5_K 从 0.711 TF 升到 4.933 TF,合 6.94 倍;q6_K 从 1.009 TF 升到 5.095 TF,合 5.05 倍;q4_K 从 1.064 TF 升到 4.973 TF,合 4.67 倍;q2_K 从 1.198 TF 升到 4.511 TF,合 3.77 倍。
4096×4096 乘 4096×4096 的倍数略低但同样显著:q3_K 6.37 倍、q5_K 6.36 倍、q6_K 4.58 倍、q4_K 4.26 倍、q2_K 3.55 倍。
旧 repack 方案在 Q4_K 上只有 2.30 到 2.32 倍(8192 与 4096 两种方阵),也就是说分块方案在同样负载上比 repack 再快约 2 倍。
前置部署 FDE 常关心的数值风险可以直接查:8192 立方下 tiled 路径的最大单点误差落在 3.05e-04 到 5.87e-04,整张张量的 RMSE 落在 2.94e-05 到 6.45e-05;作者概括为最大单标量误差约 5e-04、RMSE 约 6e-05。
测试文件里定义的容差是 1e-3(量化类型)、1e-4(F32)以及高维用例的 1e-3 乘尺度,超过容差即判 FAIL,所以这些误差不是估计值而是回归门禁的一部分。
收益边界:32 行附近盈亏平衡
Forward Deployed Engineer 应当把激活行数当成选型输入:收益随 src1 的行数缩小而消失,作者给出的盈亏平衡点在 32 行附近,这也是 FDE 判断负载能否受益的分界线。
基准表里 src1 行数为 64 时已经只剩 1.05 到 1.38 倍,行数 32 时是 0.90 到 1.02 倍,行数 16 时是 0.82 到 0.94 倍。
纯 GEMV 也就是 src1 只有 1 行时,tiled 路径只有原路径的 0.78 到 0.84,也就是慢了 16% 到 22%,作者的表述是落到 stock 性能的 80%。
合并进主干的收益门槛函数叫 ggml_tiled_min_batch,条件是 src1 的行数大于等于 8,或者被强制打开;注意 PR 描述里写的门槛仍是小于 64 行不划算,两者并不一致。
前置部署 FDE 读这张表的正确方式是:长上下文预填充、批量评估、离线索引这类一次喂很多 token 的负载能拿到 3 到 7 倍,而单用户逐 token 对话几乎只剩持平到负 20%。
支持范围与三条指令集路径
前置部署 FDE 选型时先看类型门:支持范围在 ggml_tiled_supported 里硬编码为 13 种:Q2_K、Q3_K、Q4_K、Q5_K、Q6_K,以及 IQ1_S、IQ1_M、IQ2_XXS、IQ2_XS、IQ2_S、IQ3_XXS、IQ3_S、IQ4_XS。
src1 只接受 F32 或 Q8_K 两种类型,且当 src1 是 Q8_K 时必须连续内存,否则直接回退到原路径。
指令集有三条路径:AVX512VNNI 同时需要 AVX512VL 与 AVX512DQ,走 _mm512_dpbusd_epi32 做广播点积;其次是 AVX2;再其次是仅 AVX。
非 x86 平台在编译期就被排除:ggml_tiled_supported 在既没有 AVX512VNNI、也没有 AVX2 与 AVX 的构建里直接返回 false,因此 ARM 服务器、Apple Silicon 与 Jetson 类设备完全拿不到这条优化。
作者明确说明主体是 portable C++,架构相关的只有微内核与三个内联的位解包例程,所以扩展到 ARM 的代码量很小,但本轮尚未做。
VNNI 路径额外需要把 q8_K 重新打包成连续可读布局,这段改动被限制在 VNNI 分支里,AVX2 路径不受影响。
作者同时写明一个隐含假设:代码假设 K 除以 256 余数为 0,理由是所支持的全部量化类型都使用 QK_K 等于 256。
配置与复现步骤
控制这条路径的是两个环境变量,名字是 GGML_CPU_TILED_MM 与 GGML_CPU_TILED_MM_FORCE。
GGML_CPU_TILED_MM 是总开关,默认打开,只要它存在且不等于 0 就保持启用,设为 0 时立即回退到原来的 vec_dot 路径。
GGML_CPU_TILED_MM_FORCE 只用于测试与基准,设为 1 时即使在收益为负的行数上也会强行走分块路径。
两个变量都只读一次并用 std::call_once 缓存,所以必须在进程启动前设置,进程跑起来之后再改环境变量没有任何效果。
官方的正确性用例与基准都在 test-tiled-mulmat 里:不带参数运行是正确性检查,带 --bench 进入基准表,带 --fuzz 跑随机用例。
构建与基准命令可以照抄:先执行 cmake -B build -DLLAMA_BUILD_TESTS=ON,再执行 cmake --build build -j,最后运行 ./build/bin/test-tiled-mulmat --bench;测试二进制自己会设置 GGML_CPU_TILED_MM=1 与 GGML_CPU_TILED_MM_FORCE=1。
在真实模型上对比时用 llama-bench:预填充用 -p 4096 看提升,解码用 -n 128 看是否退化,想确认差异来源就在命令前加 GGML_CPU_TILED_MM=0 跑一遍基线。
Forward Deployed Engineer 可以把这两个变量写进部署清单,回滚只有一步:把 GGML_CPU_TILED_MM 设为 0 并重启进程;如果整机版本都要退,回到 b11194 或更早的构建即可,因为这条路径不引入格式或数据的持久化变更。
作者承认的局限与未覆盖场景
第一,前置部署 FDE 接手 IQ 系列权重前要注意,作者自述当前只覆盖 k-quant 类型,并说明 IQ 系列的 codebook 边界没有逐个核对过,无法保证全部落在解包瓦片支持的范围里;这一点与合并代码里已经放出的 7 个 IQ 类型存在张力,使用者应把 IQ 类型视为未经作者完整验证。
第二,Forward Deployed Engineer 需要在交付说明里写明 ARM 与其它非 x86 平台在本轮零收益,而企业级隔离环境的边缘节点常用 ARM,这是最需要提前确认的选型约束。
第三,解码阶段会变慢 16% 到 22%,任何以逐 token 交互为主的产品形态都不该指望这次升级。
第四,全部基准来自单机 8 线程 AMD 9950x3D 桌面机,作者自己说明只用了 8 线程而非 16 线程来减少系统干扰,因此这些倍数不能直接外推到生产服务器或容器配额受限的环境。
第五,收益门槛在 PR 描述与合并代码之间不一致,描述说小于 64 行不划算,代码里是大于等于 8 行;作者承诺后续补充 llama-bench 数据,但 PR 里目前只有自建测试的表。
第六,每线程固定 512 KB 工作区意味着内存开销随线程数线性上升,在内存吃紧的容器里需要重新核对 limit。
第七,作者在 PR 里披露解包器与微内核代码由 AI 协助生成,原始思路与初版证明来自本人,这属于本次合并的一手披露而不是评价。
对前置部署工作的实用结论
前置部署 FDE 里该行动的是两类团队:在只有 CPU 的机器上跑长上下文预填充的团队,以及在做隔离环境离线推理的团队,前者可以直接吃到 3 到 7 倍。
Forward Deployed Engineer 的落地步骤只有三步:把 llama.cpp 升到 b11195 或更新;在目标机器上用 llama-bench 的 -p 4096 与 -n 128 各跑一次,与 GGML_CPU_TILED_MM=0 的基线对比;确认预填充变快且解码没有明显退化后再上生产。
适用条件要写清:x86 平台且编译时启用了 AVX2 或 AVX512VNNI、权重是 k-quant、单次乘法的激活行数不小于 8、K 是 256 的倍数。
不适用的场景不要勉强:ARM 节点、纯解码服务、以及激活行数长期小于 32 的负载应保持默认状态,或者直接把 GGML_CPU_TILED_MM 设为 0 以避免负收益。
前置部署 Forward Deployed Engineer(FDE)可以把这条改动当成一次标准的现场性能验证演练:先确认指令集,再用官方基准复现倍数,最后用环境变量留一条可回滚的开关。
前置部署 FDE 还应把门槛与描述不一致这类细节写进交付文档,因为客户环境的负载形态通常与作者桌面机不同,实测优先于引用倍数。
原始来源:
https://github.com/ggml-org/llama.cpp/releases/tag/b11195
https://github.com/ggml-org/llama.cpp/pull/27851
https://patch-diff.githubusercontent.com/raw/ggml-org/llama.cpp/pull/27851.diff
https://github.com/ggml-org/llama.cpp/blob/master/tests/test-tiled-mulmat.cpp
评论(0)
- 暂无评论。