ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入解析 Q6_0 的 MMQ CUDA Kernel:从 ik_llama.cpp PR 114 的失败尝试到完整实现

深入解析 Q6_0 的 MMQ CUDA Kernel:从 ik_llama.cpp PR 114 的失败尝试到完整实现 人工智能大模型推理引擎本地部署模型量化模型优化【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp点击查看免费下载导读本文以 ik_llama.cpp 仓库中 GitHub 讨论记录 PR #114MMQ Kernel for Q6_0 为线索完整剖析 Q6_0 量化格式在 CUDA 端 MMQ多矩阵乘法路径上的实现原理与历史演进。读者将理解 Q6_0 的位级数据布局、MMQ 与 MMVQ 两条执行路径的差异、DP4A 点积指令的数学细节以及为什么照抄 Q5_0 模板会导致困惑度暴涨 30000 倍——并最终看到当前仓库中 Q6_0 MMQ 的完整源码实现。一、事件背景一份未能合并的 PR2024 年 11 月 20 日社区贡献者Nexesenex在 ik_llama.cpp 提交了 PR #114标题是MMQ Kernel for Q6_0 (pretty please!)——一个语气谦逊的请求希望为 Q6_0 量化类型补充 CUDA MMQ kernel。该 PR 最终以Closed关闭状态结束其自述经历极具代表性作者以 Q5_0 的 kernel 为模板改写 Q6_0 版本代码可以编译、可以运行但在纯 Q6_0 量化的 Sheared Llama 2 2.7B 模型上强制使用 MMQ 后perplexity 从预期的 7~8 暴涨到 200k约 30000 倍作者在评论中坦言Its hard. Too hard for me still.太难了超出我的能力范围并提到自己在convert.cu中找不到 Q5_0 的 Cublas 模板参照无法理解转置逻辑。这是一个典型的高复杂度self-reported: Highkernel 开发案例。值得注意的是在当前版本的仓库源码中Q6_0 的 MMQ 路径已经完整实现——本文后半部分将结合 mmq.cuh 与 vecdotq.cuh 的源码还原这一实现的全貌。二、先修概念MMQ 与 MMVQ 是什么在 ggml 的 CUDA 后端中量化权重的矩阵乘法被拆分为两条内核路径路径全称计算形态典型适用场景MMVQmul_mat_vec_q单个向量 × 量化矩阵单 token 解码token generationMMQmul_mat_qmulti-token多个向量同时 × 量化矩阵即批量矩阵乘prompt 处理prefill、大 batch 场景MMQ 之所以更复杂是因为它需要将量化后的权重块x与量化后的激活y以平铺tile的形式加载到共享内存shared memory中再做点积。以 mmq.cuh 中定义的回调类型可以看到 MMQ kernel 的三个核心阶段typedef void (*load_tiles_mmq_t)(const char * __restrict__ x, int * x_tile, const int kbx0, const int i_max, const int stride); typedef void (*vec_dot_mmq_t)(const int * __restrict__ x, const int * __restrict__ y, float * __restrict__ sum, const int k00); typedef void (*mmq_write_back_t)(const float * __restrict__ sum, float * __restrict__ dst, const int stride, const int i_max, const int j_max);即加载 x 平铺 → 对每个 tile 做向量点积 → 写回结果。而 Q6_0 之所以难写问题恰恰出在第一步加载平铺的位解码上。三、Q6_0 的位级数据布局难点根源Q6_0 是 llama.cpp 家族中一种对称量化格式其 block 定义位于 ggml/src/ggml-common.h#define QK6_0 32 typedef struct { ggml_half d; // 1 个 fp16 缩放因子 uint8_t qh[QK6_0/4]; // 56-th bit of quants高位比特8 字节 uint8_t qs[QK6_0/2]; // nibbles / quants低 4 位16 字节 } block_q6_0; static_assert(sizeof(block_q6_0) sizeof(ggml_half) QK6_0/2 QK6_0/4, wrong q6_0 block size/padding);关键常量ggml-common.h#define QI6_0 (QK6_0 / (4 * QR6_0)) // 32 / 8 4每个 Q6_0 block 包含 32 个量化值每个值占6 bit低 4 位存放在qs每字节存 2 个值第 5、6 位存放在qh。这正是与 Q5_05 bit/值本质不同的地方——Q6_0 的权重需要跨两个数组qsqh重组而 Q5_0 只需处理qs4 位qh1 位。PR #114 作者以 Q5_0 为模板却未能正确理解qh的高位合并逻辑因此编译能通过、数值却完全错误——困惑度 200k 正是位组装错误的直接后果。四、当前仓库的 Q6_0 MMQ 实现全貌4.1 类型特化入口mmq_type_traits在 mmq.cuh 中Q6_0 被显式特化并接入 MMQ 体系template int mmq_x, int mmq_y, int nwarps, bool need_check struct mmq_type_traitsmmq_x, mmq_y, nwarps, need_check, GGML_TYPE_Q6_0 { static constexpr load_tiles_mmq_t load_tiles load_tiles_q6_0mmq_y, nwarps, need_check; static constexpr vec_dot_mmq_t vec_dot_mma vec_dot_q8_0_q8_1_mmammq_x, mmq_y, nwarps, MMQ_Q8_1_DS_LAYOUT_D4; static constexpr vec_dot_mmq_t vec_dot_dp4a vec_dot_q8_0_q8_1_dp4ammq_x, mmq_y, nwarps; };同时在 dispatch 层可以看到 Q6_0 被路由到 Q8_0 风格的内核族mmq.cuh 与 mmq.cuhcase GGML_TYPE_Q6_0 : return MMQ_DP4A_TXS_Q8_0; case GGML_TYPE_Q6_0 : return MMQ_MMA_TILE_X_K_Q8_0;并且在文件尾部有DECL_MMQ_CASE(GGML_TYPE_Q6_0)mmq.cuh负责生成具体的 kernel 实例。4.2 平铺加载内核load_tiles_q6_0这是 PR #114 作者失败的核心环节。当前实现位于 mmq.cuh其要点const int ql get_int_b2(bxi-qs, kqsx); // 取 qs 中的 2 字节 const int qh get_int_b2(bxi-qh, kqsx%2) 4*(kqsx/2); // 取 qh 中的高位 int qs0 ((ql 0) 0x0F0F0F0F) | ((qh 4) 0x30303030); int qs1 ((ql 4) 0x0F0F0F0F) | ((qh 2) 0x30303030); qs0 __vsubss4(qs0, 0x20202020); // subtract 32 qs1 __vsubss4(qs1, 0x20202020); // subtract 32这里体现了 Q6_0 解码的完整数学ql提供每个值的低 4 位nibble通过0x0F0F0F0F掩码提取qh提供第 5、6 位通过左移4或2位再与0x30303030二进制0011 0000掩码对齐两个片段用|合并后减去0x20202020即 32完成对称量化偏移——Q6_0 的量化值范围是[-32, 31]。注意该函数同时服务于两条子路径当INT8_MMA_AVAILABLE时写入MMQ_MMA_TILE_X_K_Q8_0布局张量核心 MMA否则写入 dp4a 布局x_qs[i*(2*WARP_SIZE 1) ...]。这与 mmq.cuh 中定义的MMQ_DP4A_MAX_BATCH_SIZE 64、MMQ_ITER_K 256、MMQ_NWARPS 8等宏一起构成了 kernel 的调度参数。4.3 点积内核vec_dot_q6_0_q8_1在向量点积层面Q6_0 × Q8_1 的实现位于 vecdotq.cuhstatic __device__ __forceinline__ float vec_dot_q6_0_q8_1( const void * __restrict__ vbq, const block_q8_1 * __restrict__ bq8_1, const int kbx, const int iqs) { const block_q6_0 * bq6_0 (const block_q6_0 *) vbq kbx; int vl[VDR_Q6_0_Q8_1_MMVQ]; // 低 4 位 int vh[VDR_Q6_0_Q8_1_MMVQ]; // 第 5、6 位 int u[2*VDR_Q6_0_Q8_1_MMVQ]; // 激活的 Q8_1 数据 for (int i 0; i VDR_Q6_0_Q8_1_MMVQ; i) { vl[i] get_int_b2(bq6_0-qs, iqs i); vh[i] get_int_b2(bq6_0-qh, i) 4*(iqs/2); u[2*i0] get_int_b4(bq8_1-qs, iqs i); u[2*i1] get_int_b4(bq8_1-qs, iqs i QI6_0); } return vec_dot_q6_0_q8_1_implVDR_Q6_0_Q8_1_MMVQ(vl, vh, u, bq6_0-d, bq8_1-ds); }其点积实现vec_dot_q6_0_q8_1_impl使用DP4A 指令ggml_cuda_dp4a一条硬件指令完成 4 组 8 位整数相乘累加int vh0i ((vh (4*i)) 4) 0x30303030; int vi __vsubss4((vil | vih), 0x20202020); // vi (vil | vih) - 32 sumf d8[i] * (ggml_cuda_dp4a(vi, u[i], 0) * sc);每条 DP4A 指令一次性处理 4 个量化值配合VDR_Q6_0_Q8_1_MMVQ 2/VDR_Q6_0_Q8_1_MMQ 4vecdotq.cuh的展开粒度实现 SIMD 化的高效点积。4.4 Q8_1 缩放布局的选择Q6_0 在 MMQ 中采用MMQ_Q8_1_DS_LAYOUT_D4布局mmq.cuh即每 32 个激活值配一个 32 位缩放因子float d4[4]。这与 Q5_0、Q8_0 一致但与 Q4_0/Q4_1 的DS4带部分和布局不同——因为 Q6_0 的量化值带符号、无 min 偏移只需纯缩放。五、PR 的动机Qwen2 的 ffn_down 形状问题PR #114 的作者在描述中给出了一个非常具体的工程动机Qwen2 系列模型广受欢迎但其ffn_down张量的形状是反转的reversed shape这导致其在 CUDA 上要么回退到 Q5_1要么使用 Q8_0——无论哪种对于 5~6 bpw 的整体量化而言质量/体积比都不理想。在 convert_hf_to_gguf.py 中可以找到对应的权重映射关系mlp.up_proj.weight: ffn_up.weight, mlp.down_proj.weight: ffn_down.weight,也就是说HuggingFace 侧的down_proj张量在转换时被原样保存为ffn_down而其形状[hidden, intermediate]而非[intermediate, hidden]与主流模型相反导致依赖特定形状假设的 kernel 分派逻辑无法为它选择最优的 MMQ 路径只能降级。社区对 Q6_0 MMQ 的诉求本质上是为了让这类非标形状张量也能享受与标准形状相同的量化内核性能。六、给 Kernel 贡献者的调试启示从 PR #114 的失败记录中可以总结出三条对任何想为量化类型编写 CUDA kernel 的开发者都适用的经验能编译、能运行≠正确PR 作者的自评显示内核跑通但困惑度 200k。位打包类内核的错误几乎不产生运行时错误而是表现为数值完全错乱必须用困惑度perplexity等端到端指标验证。PR 文档中perplexity jumps by a factor 30000就是最典型的判据。模板复用要抓住位宽这个核心差异Q5_05 bit与 Q6_06 bit看似接近但高位比特的数量与掩码完全不同。正确实现需要同时理解0x0F0F0F0F低 4 位掩码、0x30303030第 5~6 位掩码与0x2020202032 偏移三者之间的关系正如 load_tiles_q6_0 所示。参考实现已在仓库中对于后来的贡献者当前 mmq.cuh、vecdotq.cuh 中完整的 Q6_0 特化与ggml_cuda_dp4a点积实现就是最好的学习范本配套的 test-backend-ops.cpp 等测试可用于回归验证 kernel 正确性。七、总结PR #114 是 ik_llama.cpp 社区协作历史中一个有代表性的片段它记录了一次以 Q5_0 为模板、因位组装错误导致困惑度暴涨的失败尝试也折射出量化 kernel 开发的真实难度。而今天仓库中的 Q6_0 MMQ 实现——从mmq_type_traits特化、load_tiles_q6_0平铺解码到vec_dot_q6_0_q8_1的 DP4A 点积——已经完整回答了 PR 中pretty please的请求为 Qwen2 等模型的非标形状张量提供了成熟的量化计算路径也为后续量化内核开发提供了可对照的参考实现。赞分享人工智能大模型推理引擎本地部署模型量化模型优化【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp点击查看免费下载相关推荐ik_llama.cpp 的 Q6_0 量化类型从 KV-Cache 应用到 CUDA MMQ 内核的完整实战指南ik_llama.cpp 的 Q6_0 量化类型从 KV Cache 应用到 CUDA MMQ 内核的完整实战指南 ik_llama.cpp 通过 PR 77人工智能大模型推理引擎本地部署模型量化模型优化ik_llama.cpp 的 MMQ for Q6_0为 6-bit 量化补上矩阵乘加速内核ik_llama.cpp 的 MMQ for Q6_0为 6 bit 量化补上矩阵乘加速内核 ik_llama.cppllama.cpp 的一个 fork人工智能大模型推理引擎本地部署模型量化模型优化ik_llama.cpp CUDA MMQ 核微优化实录PR 567 的 tile 加载与乘法内核改进及 MMQ 架构反思ik_llama.cpp CUDA MMQ 核微优化实录PR 567 的 tile 加载与乘法内核改进及 MMQ 架构反思 导读 本文基于 ik_llama.人工智能大模型推理引擎本地部署模型量化模型优化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进