拓冰建站拓冰建站
首页 / 资讯中心 / 正文

深入解析 ik_llama.cpp PR 87:IQ3_K Metal 点积内核的未对齐读取修复与 ~2.5% 性能优化

深入解析 ik_llama.cpp PR #87IQ3_K Metal 点积内核的未对齐读取修复与 ~2.5% 性能优化【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp本文围绕 ik_llama.cpp 仓库中已关闭的 Pull Request #87iq3_k: fix and optimize Metal dot product展开源码级剖析先讲清 IQ3_K 量化块的真实内存布局与 4 字节对齐假设之间的矛盾再还原 Metal 与 CUDA 在未对齐访问上的行为差异前者静默产出垃圾、后者直接报错最后结合当前仓库中ggml-metal.metal的最新实现说明修复后的正确解码路径以及用 threadgroup 共享内存替换 constant 内存查找带来的约 2.5% 点积性能提升。读完本文你将掌握 Metal 量化内核调试的关键思路、IQ3_K 家族的数据排布细节以及如何在苹果平台规避这类延迟出现的隐性错误。一、PR 概况一个迟到的垃圾输出Bug该 PR 由项目作者 ikawrakow 于 2024-10-14 创建并同日关闭状态为 Closed。PR 描述非常简短但极具代表性原文核心信息如下作者此前按 4 字节对齐的方式访问 scales但IQ3_K数据本身并不满足 4 字节对齐。在 CUDA 上这类错误通常会直接抛出错误而 Metal 会静默接受并产生垃圾输出——而且垃圾不是立刻出现而是在已经生成若干 token 之后才出现。这段话点出了两个关键事实根因内核代码对IQ3_K的 scales 字段做了错误的 4 字节对齐访问假设危害隐蔽Metal 不报错且错误结果延迟显现导致问题极难被快速定位——模型开头输出正常跑一段时间后开始胡言乱语容易被误判为模型质量问题。此外该 PR 还顺手对 Metal 点积内核做了一项小优化作者声称带来约2.5% 的速度提升。二、背景知识IQ3_K 量化块的真实内存布局2.1 类型定义在 ggml/include/ggml.h 中IQ3_K被定义为GGML_TYPE_IQ3_K 138属于 k-quant基于 K-means 的 3-bit 量化族。它与后续出现的IQ3_KS156、IQ3_KT154、IQ3_K_R4338共同组成该仓库特有的 IQ3 量化家族。IQ3_K 的块大小由 ggml/src/ggml-common.h 中的#define QK_K 256决定即每个块处理 256 个权重。2.2 块结构体block_iq3_k当前仓库中block_iq3_k的定义位于 ggml/src/ggml-common.htypedef struct { ggml_half d; // 偏移 02 字节块级缩放因子 uint16_t extra; // 偏移 22 字节附加索引高位信息 uint16_t scales_h; // 偏移 42 字节scales 高位符号位 uint8_t scales_l[QK_K/32]; // 偏移 68 字节scales 低位 uint8_t qs[QK_K/4]; // 偏移 1464 字节低位量化索引 uint8_t qh[QK_K/8]; // 偏移 7832 字节高位量化索引 } block_iq3_k;并伴随一个编译期静态断言static_assert(sizeof(block_iq3_k) sizeof(ggml_half) 2*sizeof(uint16_t) QK_K/32 QK_K/4 QK_K/8, wrong iq3_k block size/padding);即块总大小为2 4 8 64 32 110字节没有尾部 padding。2.3 对齐问题的由来把各字段的起始偏移整理成表字段类型起始偏移字节数dggml_half02extrauint16_t22scales_huint16_t42scales_luint8_t[8]68qsuint8_t[64]1464qhuint8_t[32]7832可以看到scales_l从字节偏移 6 开始而 6 对 4 取模余 2并非 4 字节对齐边界。同时 scales 的完整信息由scales_l低位scales_h高位符号位共同承载本质上是一段跨字段、跨字节打包的紧凑位域而非一个规整的uint32_t数组。这正是 PR 描述中所说IQ3_K 不是 4 字节对齐的精确含义如果内核图省事把 scales 当作连续的 4 字节整数来读就会产生未对齐的内存访问。三、Bug 的放大镜CUDA 报错 vs Metal 静默出错作者在 PR 中对比了两个后端的错误处理差异这是一个对量化内核开发极有价值的观察CUDA出现这类访问错误时驱动/运行时通常会触发错误如 illegal memory access开发者能立刻在日志或调试器中看到失败问题容易定位Metal编译器按已对齐的假设生成加载指令运行时不校验、不抛错只是读到错误的字节组合最终表现为数值错误garbage。更麻烦的是垃圾输出的延迟性由于 IQ3_K 的 256 元素块分散在矩阵的不同行、不同块中未对齐读取导致的字节错位只会影响部分块的计算结果。解码开始时模型本身的自回归特性强先验、短上下文会掩盖这些错误随着 token 累积错误逐 token 放大、互相污染最终在几十上百个 token 后表现为明显的退化输出。这正是不能一眼看出 bug的根本原因。四、修复后的正确实现当前源码中的解码路径PR #87 已关闭其修复成果体现在当前仓库的 Metal 内核中。以IQ3_K的矩阵-向量乘mul_mv内核kernel_mul_mv_iq3_k_f32_impl为例其完整实现位于 ggml/src/ggml-metal.metal对外入口为kernel_mul_mv_iq3_k_f32ggml/src/ggml-metal.metal。4.1 不再假设 4 字节对齐的 scales 读取修复后的代码在 ggml/src/ggml-metal.metal 中以 16 位指针逐段读取 scales 并手工拼接全程避开 4 字节对齐要求device const uint16_t * ql16 (device const uint16_t *)xb.qs 16*iq 4*ir; device const uint16_t * qh16 (device const uint16_t *)xb.qh 4*ir; device const uint16_t * sc16 (device const uint16_t *)xb.scales_l; uint32_t scales32 sc16[2*iq0] | (sc16[2*iq1] 16); scales32 ((scales32 4*is) 0x0f0f0f0f) 1; thread const int8_t * s8 (thread const int8_t *)scales32; uint16_t extra (xb.extra (8*iq is)) 3; uint16_t signs xb.scales_h (8*iq is);关键点解读scales_l虽以uint8_t数组存储偏移 6非 4 字节对齐但这里用uint16_t *指针按 2 字节粒度读取再将两段 16 位拼成 32 位scales32从数据读取层面规避了未对齐 4 字节加载extra来自独立的uint16_t extra字段提供权重索引的高位比特signs来自uint16_t scales_h提供每组 scale 的符号位最终在累加阶段通过signs 0x01 / 0x04 / 0x10 / 0x40决定-s8[i]还是s8[i]见 ggml/src/ggml-metal.metal。4.2 权重索引与 k-values 查找表IQ3_K 的权重并不是直接存数值而是存索引解码时经查找表还原。当前内核使用常量表constexpr constant static float kvalues_iq3k_f[16] { -63.f, -40.f, -23.f, -10.f, 1.f, 13.f, 28.f, 47.f, -59.f, -36.f, -19.f, -6.f, 5.f, 17.f, 32.f, 51.f };定义于 ggml/src/ggml-metal.metal。内层循环中权重索引由vl/vh的低位组合而成aux32[0] (vl[0] 0x03030303) | (vh[0] 0x04040404)见 ggml/src/ggml-metal.metal得到一个 0~7 的三位索引再叠加extra 8提供的第四位共同索引 16 项的kvalues_iq3k_f表。五、顺手优化threadgroup 共享内存取代 constant 内存查找5.1 优化手法PR 附带约 2.5% 速度提升的优化在代码中留下了清晰的痕迹。优化前内层循环直接查 constant 内存//constant float * values kvalues_iq3k_f (extra 8); // 旧实现已被注释优化后kvalues_iq3k_f表被预先拷贝进 threadgroup共享内存每个 simdgroup 独占 16 个 float 槽位threadgroup float * all_values (threadgroup float *)shared_values 16*sgitg; { if (tiisg 16) all_values[tiisg] kvalues_iq3k_f[tiisg]; simdgroup_barrier(mem_flags::mem_none); }见 ggml/src/ggml-metal.metal。内层热点循环则改为threadgroup const float * values all_values (extra 8);见 ggml/src/ggml-metal.metal。5.2 为什么更快该查表位于最内层循环对每个块、每个 4 元素组执行是典型的高频率、多线程并发随机访问场景constant 内存虽然配有缓存但在同一 warp/simdgroup 内不同线程访问不同地址时constant 路径的广播/序列化特性会限制吞吐threadgroup 内存共享内存支持更灵活的每线程独立寻址把这张 16 项小表放到共享内存后内层循环的取数延迟更低、吞吐更高。把表拷贝到共享内存只需在内核开头做一次16 次写入 一次 barrier代价极小而收益发生在后续每一个块的循环中因此整体获得约 2.5% 的净加速。这是一个非常典型的移动不变量、降低热点访存代价的 Metal 内核微优化案例。六、IQ3_K 家族与更多相关内核本次修复针对的是IQ3_K但同一家族在仓库中还有多个变体它们各自拥有独立的 Metal 内核与 CPU/CUDA 实现变体Metal 内核mul_mv类型枚举ggml/include/ggml.hIQ3_Kkernel_mul_mv_iq3_k_f32ggml-metal.metal138IQ3_KSkernel_mul_mv_iq3_ks_f32ggml-metal.metal156IQ3_KTkernel_mul_mv_iq3_kt_f32ggml-metal.metal154IQ3_K_R4由block_iq3_k_r44 个block_iq3_k打包见 ggml-common.h支撑338此外IQ3_K在 Metal 后端还配套了其他算子模板反量化dequantize_iq3_kggml-metal.metal取行get_rowskernel_get_rows_iq3_kggml-metal.metal矩阵-矩阵乘mul_mmkernel_mul_mm_iq3_k_f32/_f16ggml-metal.metal、ggml-metal.metal以及 MoE 专家并行的kernel_mul_mm_id_iq3_k_f32ggml-metal.metal。CPU 侧与 CUDA 侧也分别有对应实现量化/反量化/点积接口声明在 ggml/src/iqk/iqk_quantize.h如quantize_row_iq3_k、dequantize_row_iq3_k、vec_dot_iq3_k_q8_kCUDA 端则分布在 ggml/src/ggml-cuda/mmq.cuh、ggml/src/ggml-cuda/dmmv.cu 及 template-instances 目录下的mmq-instance-iq3_k_id.cu、mmvq-instance-iq3_k.cu等文件中。七、如何验证这类延迟垃圾输出如果你在苹果设备上使用 IQ3_K 量化模型并怀疑出现同类问题可以从以下方向验证长序列压测这类错误通常需要几十到上百个 token 才显现务必运行足够长的生成任务。仓库中的 examples/perplexity困惑度评测和 examples/main常规对话生成都很适合做回归测试量化数据自检用 examples/quantize-stats 检查量化后的统计信息或使用 examples/quantize 重新量化同一模型对比不同批次输出是否一致后端对照同一模型在 Metal 与 CUDA/CPU 上分别跑同一 prompt若 Metal 后端输出明显退化而其他后端正常优先怀疑 Metal 内核的访存/位运算逻辑注意编译期断言block_iq3_k带有static_assert校验块大小与 paddingggml-common.h任何对结构体的改动若破坏布局都会在编译期暴露这是防止同类对齐问题复发的重要防线。八、总结PR #87 是一个小而精的经典案例一个 4 字节对齐假设在 CUDA 上会立刻报错、在 Metal 上却静默产生延迟垃圾输出而修复本身改用 16 位分段读取 scales与优化threadgroup 内存替代 constant 内存查表加起来不过数行代码却同时解决了正确性与约 2.5% 的性能问题。对任何在 Metal 上开发量化内核的开发者而言这个案例都提醒我们严格遵守数据结构的实际对齐要求并善用共享内存移动内层循环的不变量——这既是正确性的底线也是性能优化的富矿。【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门