十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ik_llama.cpp 的 CUDA 更优 MoE 实现:以可复现的间接矩阵乘法换取 Prompt Processing 性能跃升

ik_llama.cpp 的 CUDA 更优 MoE 实现:以可复现的间接矩阵乘法换取 Prompt Processing 性能跃升 ik_llama.cpp 的 CUDA 更优 MoE 实现以可复现的间接矩阵乘法换取 Prompt Processing 性能跃升【免费下载链接】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 #283「CUDA: better MoE implementation」为技术主线剖析其如何解决 DeepSeek 等大规模 MoEMixture-of-Experts模型在 CUDA 后端推理时结果不可复现non-reproducible的问题并顺带获得约 10% 的 Prompt ProcessingPP吞吐提升。读者将掌握 MoE 推理中mul_mat_id间接矩阵乘法的性能瓶颈成因、基于 row-id 预计算的新实现思路以及用llama-bench/llama-perplexity复现该 PR 收益的完整实操方法。一、PR 背景MoE 模型在 CUDA 上的结果不可复现问题1.1 一个被关闭的 issue#249PR #283 的首要目标是修复 issue #249 描述的缺陷MoE 模型推理时使用的「间接」矩阵乘法indirect matrix multiplications在 CUDA 上结果不可复现。也就是说同一模型、同一输入、同一随机种子多次运行得到的输出可能不同。这种不确定性对需要严格可复现的评测如 perplexity 对比、量化效果评估是致命的——用户无法判断结果差异来自代码改动还是浮点顺序抖动。该 PR 于 2025-03-24 创建2025-04-05 完成代码审查后合并作者为项目维护者ikawrakow。1.2 为什么 MoE 需要「间接」矩阵乘法在 GGML/llama.cpp 的算子体系中MoE 专家的前向传播依赖GGML_OP_MUL_MAT_ID矩阵乘加 id 索引。与普通矩阵乘法不同它有三个输入src0全部专家的权重张量形状为[n_embd, n_embd_gate, n_experts]即按专家维度堆叠src1激活向量形状为[n_tokens, n_embd, n_sequences]ids形状为[n_experts_per_token, n_tokens]的整数索引标明每个 token 实际调用了哪几个专家。由于每个 token 只激活全部n_as个专家中的少数几个如 DeepSeek 系列每 token 只激活 8 个计算必须按照ids索引「间接」地从专家权重中取行因而得名。在 ggml/src/ggml-cuda.cu#L2874-L2877 的ggml_cuda_mul_mat_id入口处可以看到该算子正是从dst-src[0]专家权重、dst-src[1]激活、dst-src[2]ids三者取数的static bool ggml_cuda_mul_mat_id(ggml_backend_cuda_context ctx, ggml_tensor * dst, ggml_tensor * next) { const ggml_tensor * src0 dst-src[0]; const ggml_tensor * src1 dst-src[1]; const ggml_tensor * ids dst-src[2];二、性能与不确定性的共同根源k_copy_src1_to_contiguous内核PR 描述直指问题核心罪魁祸首是k_copy_src1_to_contiguous内核。它在把激活矩阵src1按专家重新排列成连续内存contiguous copy时使用了一个atomic increment原子自增来分配目标位置慢原子操作在 GPU 上串行化竞争多个线程同时自增同一个计数器会严重拖慢内核吞吐不确定原子自增的完成顺序是随机的导致src1各行写入连续缓冲区的顺序随机化从而使后续浮点累加顺序逐次运行不同最终输出不可复现。更糟的是这个内核被调用了n_as次其中n_as是专家总数。对于 DeepSeek-V3 / R1 / Lite 这类拥有 256 个专家的模型这意味着每次mul_mat_id都要串行执行数百次低效的原子竞争拷贝直接拖垮 PP 阶段吞吐。这正是 PR 标题中 better MoE implementation 要解决的问题。2.1 从当前源码看新方案的落地在合并后的仓库中这一旧路径已不复存在取而代之的是两套新实现量化专家的专用路径mmq_id见 ggml/src/ggml-cuda/mmq_id.cu当src0是量化类型如 IQ 系列、Q8_0 等时ggml_cuda_mul_mat_id在满足条件src1-ne[2] ctx.mmq_id_thresh*src0-ne[2]见 ggml/src/ggml-cuda.cu#L2944时进入ggml_cuda_mul_mat_q_idmmq_id.cu#L317。行重排的确定性计算新方案不再用原子增量现场拼装连续缓冲区而是先用mmq_ids_helper内核mmq_id.cu#L34把ids转换成更便于计算的形式一次性生成三张确定性映射表ids_src1描述如何重排src1的展平列索引得到按专家排序的紧凑src1ids_dst描述同一映射如何作用于输出dstexpert_bounds每个专家在重排后缓冲区中的边界。compute_row_idsmmq_id.cu#L277针对每 token 专家数 2/4/6/8/16/32 分别实例化模板特化版本并约束n_tokens 2^22、n_expert_used 2^10以保证 32 位打包安全mmq_id.cu#L128-L129。这套「先算 row id再按确定性映射执行重排与归约」的流程彻底消除了原子增量带来的随机顺序与竞争开销从源码结构上可以推断这正是 PR #283 描述的方法在合并后仓库中的延续与进一步优化。三、性能收益PR 声明与社区实测3.1 PR 声明PR 描述中作者给出的收益作为附带收益在 DeepSeek-Lite 上测得约 10% 的 PP 加速。考虑到 DeepSeek-R1 的专家数量是 DeepSeek-Lite 的 4 倍其收益很可能更大。这与 MoE 模型特性一致专家数越多k_copy_src1_to_contiguous被调用的次数越多消除后的收益越明显。3.2 社区实测ubergarm单卡 RTX A6000贡献者ubergarm在 Threadripper Pro 24 核 单张 RTX A6000 48GB 的环境上对 DeepSeek-R1 IQ2_K_R4226 GiB672B 参数做了llama-bench与llama-perplexity对比llama-bench结果baselinebuild 3607与 PR #283build 3610的 PP512 / PP4096 / TG 各档 t/s 几乎一致如 pp512 为 105.50 vs 105.85 t/spp4096 为 99.93 vs 99.64 t/s无显著差异两次llama-perplexity的最终 PPL 均为3.6989 /- 0.02106逐 token 的困惑度序列也完全一致复现性良好。作者ikawrakow随即解释了原因该测试通过--override-tensor expsCPU把 MoE 专家放到了 CPU 上执行因此看不到 GPU 侧 MoE 实现的差异这本身也验证了改动没有引入回归。要看到收益至少需要有一部分专家在 GPU 上运行。3.3 社区实测davidsyoung16×RTX 3090多卡用户davidsyoung在 16×3090 配置上对 DeepSeek-R1Q8_0307.2 GiB运行了包含 PP512PP8192 与 TG128TG2048 全档位的llama-bench并给出了结论性评价 Awesome improvement!。作者也指出PR 合并后ik_llama.cpp对 DeepSeek-V3/R1/Lite 这类多专家 MoE 模型的 PP 性能约为 mainline 的 1.8 倍在 davidsyoung 的多卡系统上约为 vLLM 的 80%90%——且 vLLM 使用了 tensor parallelism而 ik_llama.cpp 当时尚未对 MoE 模型启用 row split作者认为补上这一点即可追平甚至超越 vLLM。以上数字均引自该 PR 讨论区属于特定软硬件环境下的测量结论不应泛化为普遍性能承诺。四、实战复现用 llama-bench 与 llama-perplexity 验证收益4.1 环境与模型复现 PR 效果需要满足两个前提至少部分 MoE 专家在 GPU 上运行否则看不到差异见 3.2 节的教训使用支持 MoE 的量化模型如 DeepSeek-R1 / DeepSeek-Lite 的 GGUF 文件。4.2llama-bench基准命令以下是讨论区实测所用的llama-bench命令针对 DeepSeek-R1 IQ2_K_R4CUDA_VISIBLE_DEVICES0, \ ./build/bin/llama-bench \ --model /mnt/raid/models/ubergarm/DeepSeek-R1-GGUF/DeepSeek-R1-IQ2_K_R4.gguf \ -ctk q8_0 \ -mla 2 -fa 1 \ -amb 512 \ -fmoe 1 \ -p 512,4096 -n 0 \ -gp 512,64 \ -gp 4096,64 \ -r 2 \ --n-gpu-layers 63 \ --override-tensor expsCPU \ --threads 24关键参数说明均可在 examples/llama-bench/llama-bench.cpp 的-fmoe, --fused-moe 0|1默认 1见 llama-bench.cpp#L376找到对应解析逻辑参数含义默认值-ctk q8_0以 Q8_0 类型缓存 Key视模型而定-mla 2启用 MLAMulti-head Latent Attention路径0关闭-fa 1启用 Flash Attention1开启-amb 512Attention 最大 batch 大小512-fmoe 1启用 fused MoE 前向fused_moe_up_gate1开启-p 512,4096测试的 prompt 长度档位512-gp 512,64指定每档 prompt 的生成 token 数512-r 2每档重复 2 次取均值1--n-gpu-layers 63卸载到 GPU 的层数0--override-tensor expsCPU强制将 exps专家权重张量放到 CPU无注意若把exps放到 CPUPP 差异将不可见见 3.2因此完整验证 GPU 侧收益时应去掉--override-tensor或仅部分卸载专家。llama-bench会在fmoe ! 默认值时于输出中打印 fmoe 列llama-bench.cpp#L1831便于交叉核对配置。4.3llama-perplexity复现性验证命令CUDA_VISIBLE_DEVICES0, \ ./build/bin/llama-perplexity \ --model /mnt/raid/models/ubergarm/DeepSeek-R1-GGUF/DeepSeek-R1-IQ2_K_R4.gguf \ -ctk q8_0 \ -mla 2 -fa \ -amb 512 \ -fmoe \ --ctx-size 512 \ --ubatch-size 512 \ -f wiki.test.raw \ --seed 1337 \ --n-gpu-layers 63 \ --override-tensor expsCPU \ --threads 24固定--seed 1337后PR 前后两次运行的逐 chunk PPL 序列逐位一致最终PPL 3.6989 /- 0.02106——这正是「结果可复现」的直接证据。在 common/common.cpp#L1960 中-no-fmoe, --no-fused-moe可以显式关闭 fused MoE用于 A/B 对比-mla, --mla-usecommon.cpp#L1929与-fa/-no-facommon.cpp#L1909-L1914则用于开关 MLA 与 Flash Attention 路径。4.4 与 vLLM / sglang 的讨论要点讨论中社区还比较了其他推理框架vLLM 对 GGUF 与 imatrix 量化的支持有限当时对 Q3 存在溢出问题sglang 不支持 GGUF而 AWQ 量化在作者看来质量偏低。这段讨论从侧面说明了 ik_llama.cpp 在「GGUF 新量化 多专家 MoE」组合下的生态位置但它属于讨论区观点不构成对任一框架的结论性评价。五、代码审查中的技术细节cudaMemcpyAsync与内存生命周期该 PR 的代码审查由 llama.cpp 核心开发者JohannesGaessler参与围绕ggml/src/ggml-cuda.cu中移除的一处同步展开是理解 CUDA 异步语义的绝佳案例审查意见ids_host与rmapping在函数结束时出作用域被释放而cudaMemcpyAsync是异步的若不同步源指针可能已失效导致偶发段错误或拷贝到垃圾数据作者回应这两个缓冲区在后续 kernel 调用中仍被使用而这些 kernel 的排队执行依赖 memcpy 先行完成CUDA 流内保证设备代码执行顺序故移除同步是安全的作者也坦承遗留同步的原因是开发期间曾遇到越界访问 bug一度误以为是拷贝未完成结论k_copy_dst_from_contiguous只使用设备指针其数据有效性由 CUDA 流的执行顺序自动保证而cudaMemcpyAsync使用宿主指针其生命周期受宿主代码控制——这正是两者语义差异的关键。这段讨论也直接关联到后续 issue #313作者提到 See #313即宿主侧在拷贝完成前读取数据的风险。最终作者表示只要有人能实际触发该 bug他会恢复同步调用PR 仍按计划合并。六、总结PR #283 是 ik_llama.cpp 在 MoE 推理路径上的一次关键重构其技术要点可归纳为根因定位k_copy_src1_to_contiguous使用原子增量做行重排既慢又引入顺序随机性且被调用n_as专家总数次成为 PP 阶段瓶颈解决思路以确定性的 row-id 预计算mmq_ids_helper/compute_row_ids替代原子竞争先建立ids_src1/ids_dst/expert_bounds映射再执行确定性重排与归约同时消除不确定性与开销收益修复 #249 的可复现性问题并在 DeepSeek-Lite 上获得约 10% PP 加速专家更多的模型收益更大社区在 16×3090 多卡系统上确认了显著提升验证方式固定--seed的llama-perplexity用于复现性验证全档位llama-bench用于性能对比注意需让至少部分专家驻留 GPU代码位置核心实现沉淀于 ggml/src/ggml-cuda/mmq_id.cu 与 ggml/src/ggml-cuda.cu 的ggml_cuda_mul_mat_id当前仓库中仍在持续演进。对于希望深入了解 MoE 推理底层实现的读者建议沿着ggml_cuda_mul_mat_id→ggml_cuda_mul_mat_q_id→compute_row_ids→mmq_ids_helper的调用链逐层阅读配合 examples/llama-bench/llama-bench.cpp 中的-fmoe开关做 A/B 实测即可完整复现并理解本 PR 的全部技术价值。【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表