ik_llama.cpp 的 IQ1_S_R4 量化在 NEON 上的 GEMM/GEVM 提速:PR #186 源码级解析与实测数据
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
导读
本文以 ik_llama.cpp 仓库中 PR #186(iq1_s_r4: slightly faster NEON gemm/gemv)为线索,深入解析 1 比特量化格式 IQ1_S_R4 的数据布局、量化/反量化流程,以及它在 ARM NEON(Apple Silicon)上的矩阵乘(GEMM)与矩阵向量乘(GEVM)优化实现。读者将掌握:IQ1_S_R4 与 IQ1_S 的格式差异、iqk子库中mul_mat_iq1_s_r4_q8_1的内核级优化思路、PR #186 在 M2-Max 上的实测吞吐提升数据,以及如何理解这类 R4(四行打包)量化在解码(token generation)与预填充(prompt processing)阶段的性能影响。
背景:ik_llama.cpp 与 1 比特量化家族
ik_llama.cpp 是 llama.cpp 的一个 fork,定位为“附加 SOTA 量化格式并提升性能”(additional SOTA quants and improved performance)。在极端低比特量化方向,该仓库维护了一系列 IQ(“I”指 integer,即整数内核)量化格式,其中IQ1_S(1.56–1.75 bpw 级别)是面向极小模型体积的 1 比特量化格式。为了在保持量化精度的同时提升 CPU 上的计算吞吐,仓库引入了R4(row-4,四行打包)变体,例如 IQ1_S_R4、IQ1_M_R4、IQ3_S_R4 等。
PR #186 正是针对IQ1_S_R4的 ARM NEON 矩阵乘内核做的一次“slightly faster”微优化,官方实测在 DeepSeek-Lite(deepseek2 16B,IQ1_S_R4 量化)上带来约 4.6%~12.4% 的吞吐提升。本文后续章节将逐一拆解其底层原理与数据。
IQ1_S_R4 的块布局:从 block 定义看设计意图
IQ1_S_R4 的核心数据结构定义在 ggml/src/ggml-common.h 中:
typedef struct { ggml_half d; uint8_t qs[QK_K/8]; uint16_t qh[QK_K/32]; } block_iq1_s; static_assert(sizeof(block_iq1_s) == sizeof(ggml_half) + QK_K/8 + QK_K/16, "wrong iq1_s block size/padding"); typedef struct { uint8_t qs[16]; uint16_t qh[4]; } block_iq1_s_r4; static_assert(sizeof(block_iq1_s_r4) == 24, "wrong iq1_s_r4 block size/padding");关键差异解读:
- IQ1_S(上):每个
QK_K(默认 256)权重为一组,包含一个 fp16 缩放因子d、QK_K/8字节的低位索引qs和QK_K/32个 uint16 的高位索引qh,整体 1.5625 bpw。 - IQ1_S_R4(下):不再为每个块单独存缩放因子,而是将 4 行打包成一个 24 字节的块:
qs[16]+qh[4](每个qh为 uint16,即 4×16 bit)。d缩放因子被移出块外,由上层计算逻辑按 4 行一组单独处理(见下文量化函数)。
从量化实现看,ggml/src/iqk/iqk_quantize.cpp 中quantize_iq1_s_r4以nrows=4、n_per_row=k/4的方式工作,缩放因子单独写入数据流前端(dptr + 4处为 4 个 half 精度缩放):
size_t quantize_iq1_s_r4(const float * src, void * dst, int64_t nrows, int64_t n_per_row, const float * imatrix, const struct quantize_user_data * use_data) { // ... auto y = (block_iq1_s_r4 *)(dptr + 4); // ... }这种“四行共享缩放 + 紧凑位打包”的设计,使 R4 变体在相同 bpw 下能减少缩放因子的内存带宽开销,并让向量化内核一次加载 4 行的数据块做批处理(详见第 4 节)。
支持 IQ1_S_R4 的相关源码位置
| 环节 | 文件 | 说明 |
|---|---|---|
| 块布局定义 | ggml/src/ggml-common.h | block_iq1_s_r4,24 字节 |
| 量化/反量化 | ggml/src/iqk/iqk_quantize.cpp | quantize_row_iq1_s_r4_ref、quantize_iq1_s_r4 |
| 点积参考实现 | ggml/src/iqk/iqk_quantize.h | vec_dot_iq1_s_r4_q8_k等 |
| NEON/x86 矩阵乘 | ggml/src/iqk/iqk_gemm_1bit.cpp | mul_mat_iq1_s_r4_q8_1内核 |
| 模型转换 | src/llama-quantize.cpp | { GGML_TYPE_IQ1_S_R4, { GGML_TYPE_IQ1_S, 4} } |
| GGUF 类型注册 | ggml/src/ggml-backend.cpp | GGML_TYPE_IQ1_S_R4的算子分发 |
PR #186 做了什么:NEON 内核层面的微优化
PR #186 的核心改动位于 ggml/src/iqk/iqk_gemm_1bit.cpp 中的mul_mat_iq1_s_r4_q8_1。这是 IQ1_S_R4 权重与Q8_1激活做矩阵乘(GEMM)与矩阵向量乘(GEVM)的统一内核模板,函数模板参数nrc_y决定一次处理的目标行数(即输出通道数)。
template <int nrc_y> static void mul_mat_iq1_s_r4_q8_1(int n, const void * vx, size_t bx, const DataInfo& info, int nrc_x) { GGML_ASSERT(nrc_x%4 == 0); Q8<nrc_y, block_q8_K128> q8(info); int nb = n / 32; GGML_ASSERT(nb%4 == 0); __m256i qx[4]; __m256 acc[nrc_y] = {}; auto m1 = _mm256_set1_epi16(1); auto ms = _mm_set1_epi16(-32768); float d8[4*nrc_y]; // ... }从源码结构看,该内核的优化要点可归纳为以下几点:
四行并行(nrc_x%4 == 0):每次迭代同时处理 4 行权重(
ix += 4),4 个__m256i qx[4]向量分别承载 4 行的解码结果,最大化 SIMD 寄存器复用,减少权重数据的重复加载。符号位与缩放因子的合并解码:IQ1_S 的索引由低位索引(
qs)与高位符号/索引(qh)共同构成。内核先通过_mm_srli_epi16(sas, 12)提取 3 bit 缩放索引,再构造 ±1 符号并乘上缩放(signs = _mm_mullo_epi16(signs, scales4)),随后用查表iq1s_grid_us[...]将 16 个索引一次性映射为网格点(_mm256_set_epi64x组合),把 1 比特权重展开成便于整数点积的形态。整数量化点积(dpbusd/maddubs):在支持
HAVE_FANCY_SIMD(AVX-VNNI 等)的平台用_mm256_dpbusd_epi32完成 8-bit 乘累加,否则回退到_mm256_maddubs_epi16+_mm256_madd_epi16的两级归约。这与 x86 端对mul_mat_iq1_s_r4_q8_1的另一个变体mul_mat_iq1_s_r4_q8_1_1(见 iqk_gemm_1bit.cpp)保持一致,只是指令集分支不同。外层累加与延迟写回:4 行的部分和先累积在
__m256 acc[nrc_y]寄存器中,最后统一info.store(...)写回,并把 1/8 的常数归约(hsum/_mm_mul_ps(d1, sumf))合并到收尾阶段,减少写内存次数。
需要说明:PR #186 标题中的“NEON gemm/gemv”指该优化落地于
ggml/src/iqk的 CPU 内核路径(同时覆盖 x86 与 NEON 指令集分支),实测环境为 Apple M2-Max。从当前仓库源码看,该内核同时保留了 AVX 与 NEON 分支的编译路径,HAVE_FANCY_SIMD等宏决定使用哪套指令序列。
内核的注册方式见 iqk_gemm_1bit.cpp:
IQK_SET_MUL_MAT_FUNCTIONS(mul_mat_iq1_s_r4_q8_1, funcs); func16 = mul_mat_iq1_s_r4_q8_1<16>;这表示该函数会被绑定为 IQ1_S_R4 在 iqk 路径下的 GEMM/GEVM 算子,供 ggml/src/iqk/iqk_mul_mat.cpp 的统一调度层按数据类型分派。
实测性能:PR #186 在 DeepSeek-Lite 上的提升
PR #186 的描述中给出了官方在M2-Max CPU、DeepSeek-Lite(deepseek2 16B,IQ1_S_R4 量化)上的对比测试,列于下表。tg128表示生成(token generation)测试中一次处理 128 token 的批大小;pp512表示预填充(prompt processing)测试中一次处理 512 token 的批大小。t/s为吞吐(tokens per second),main为合并基线,PR为应用本 PR 后的版本。
| 模型 | threads | 测试 | t/s (main) | t/s (PR) | 加速比 |
|---|---|---|---|---|---|
| deepseek2 16B IQ1_S_R4 | 2 | tg128 | 22.76 ± 0.15 | 24.07 ± 0.19 | 1.058 |
| deepseek2 16B IQ1_S_R4 | 4 | tg128 | 37.83 ± 0.00 | 39.58 ± 0.02 | 1.046 |
| deepseek2 16B IQ1_S_R4 | 8 | tg128 | 62.01 ± 0.02 | 65.26 ± 0.82 | 1.052 |
| deepseek2 16B IQ1_S_R4 | 8 | pp512 | 251.97 ± 0.09 | 283.20 ± 0.54 | 1.124 |
从数据可以总结出三个规律(均以该 PR 实测为前提,不代表其他硬件/模型结论):
- 解码(tg)提升稳定在 4.6%~5.8%:2/4/8 线程下的加速比分别为 1.058 / 1.046 / 1.052,与 PR 标题“slightly faster”的描述一致。解码是内存带宽受限场景,微内核优化带来的寄存器级收益被换算为 5% 左右的吞吐提升。
- 预填充(pp512)提升显著,达 12.4%:
251.97 → 283.20 t/s。预填充是计算密集场景,GEMM 内核的 SIMD 效率直接决定吞吐,因此优化收益被放大。 - 多线程扩展性良好:2→8 线程时基线 22.76 → 62.01 t/s(约 2.7×),PR 版本 24.07 → 65.26 t/s,说明优化没有引入串行瓶颈。
这类 “tg 微升、pp 大增” 的分布,是低比特量化 CPU 内核优化的典型特征,也解释了为什么仓库后续围绕 R4 系列量化(IQ1_M_R4、IQ3_S_R4、IQ4_KSS 等)持续投入矩阵乘内核优化。
为什么关注 R4:从算子注册到模型转换的完整链路
IQ1_S_R4 并非只存在于内核层,它在量化与模型转换链路中被完整支持:
- 在 src/llama-quantize.cpp 中,
IQ1_S_R4被定义为IQ1_S的 R4 变体:{ GGML_TYPE_IQ1_S_R4, { GGML_TYPE_IQ1_S, 4 } },表示它是把 4 个IQ1_S块合并打包的格式。 - 同一文件(src/llama-quantize.cpp)中记录了格式转换关系:
LLAMA_FTYPE_MOSTLY_IQ1_S → LLAMA_FTYPE_MOSTLY_IQ1_S_R4,即常规 IQ1_S 模型可进一步 repack 为 R4 格式。 - GGUF 文件类型
LLAMA_FTYPE_MOSTLY_IQ1_S_R4在 src/llama-quantize.cpp 中映射到GGML_TYPE_IQ1_S_R4,最终由 ggml 后端的类型表分派到 iqk 内核。
因此,一个完整的 IQ1_S_R4 使用链路是:
HF 模型 → llama-quantize 量化为 IQ1_S_R4(或从 IQ1_S repack) → GGUF 加载(llama-model-loader) → ggml 类型分派(GGML_TYPE_IQ1_S_R4) → iqk 调度层 → mul_mat_iq1_s_r4_q8_1(NEON/AVX 内核)仓库内还保留了独立的 CPU 参考实现vec_dot_iq1_s_r4_q8_k(声明于 ggml/src/iqk/iqk_quantize.h),用于非 SIMD 平台或正确性对照;GPU 端(CUDA/MMQ、MMVQ 模板,见 ggml/src/ggml-cuda/template-instances/mmq-instance-iq1_s_r4.cu)也有对应实现,但 PR #186 的优化范围明确限定在 CPU 的 NEON gemm/gemv 路径。
如何在当前仓库中复现与验证
要在自己的机器上复现 PR #186 类似的基准,可按以下步骤操作(以 Linux/macOS + CMake 为例,前提是已安装对应编译工具链):
- 构建 ik_llama.cpp(在仓库根目录):
cmake -B build -DGGML_IQK_MULMAT=ON -DCMAKE_BUILD_TYPE=Release cmake --build build --config Release -j
GGML_IQK_MULMAT决定是否启用 iqk 矩阵乘路径;若关闭,将回退到常规点积实现,无法体现本文所述内核收益。Apple Silicon 默认启用 NEON 分支,x86 平台走 AVX/AVX-VNNI 分支。
- 准备 IQ1_S_R4 模型:使用
llama-quantize将目标模型量化为IQ1_S_R4,或对已有IQ1_S模型执行 R4 repack(对应 src/llama-quantize.cpp 的转换关系)。
./build/bin/llama-quantize <原始模型.gguf> <输出模型-IQ1_S_R4.gguf> IQ1_S_R4- 跑基准:使用仓库自带的
llama-bench(examples/llama-bench)分别测试tg128与pp512场景:
./build/bin/llama-bench -m <输出模型-IQ1_S_R4.gguf> -t 8 -b 512 -n 128将得到与 PR 描述同源的t/s指标;若与基线版本对比(如先测合并基线再测含 PR 的构建),即可复现 4.6%~12.4% 量级的加速比分布。注意 PR #186 状态为已关闭(Closed),其优化应已并入主分支,当前仓库源码(ggml/src/iqk/iqk_gemm_1bit.cpp)中的mul_mat_iq1_s_r4_q8_1即为其最终形态。
小结
PR #186 是 ik_llama.cpp 在“1 比特量化 × CPU 内核优化”方向上的一次典型微迭代:通过重构 IQ1_S_R4 的 SIMD 解码与整数量化点积路径,在 M2-Max 上为 DeepSeek-Lite 16B 带来解码约 5%、预填充约 12% 的吞吐提升。从block_iq1_s_r4的紧凑布局,到mul_mat_iq1_s_r4_q8_1的四行并行与查表解码,再到llama-quantize的格式转换链路,整个仓库为这种“低 bpw + 高吞吐”的目标提供了完整闭环。对于关注极端低比特 CPU 推理的开发者,本文梳理的源码路径(ggml-common.h、iqk_quantize.cpp、iqk_gemm_1bit.cpp)可作为继续深入 iqk 子库的起点。
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考