ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

ik_llama.cpp 多 GPU 混合卸载调优实战:-ot 半层拆分下 TG 性能骤降的根因分析与 -fmoe 的正确用法

2026/9/19 3:36:11 拓冰建站 浏览量
ik_llama.cpp 多 GPU 混合卸载调优实战:-ot 半层拆分下 TG 性能骤降的根因分析与 -fmoe 的正确用法 ik_llama.cpp 多 GPU 混合卸载调优实战-ot 半层拆分下 TG 性能骤降的根因分析与 -fmoe 的正确用法【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp本指南以 ik_llama.cpp 仓库 Issue #521 的完整排查过程为骨架深入剖析在 CUDA CPU 混合环境下使用-ot--override-tensor按张量粒度做层内拆分时为什么文本生成TG吞吐会从正常水平骤降到 1.5 t/s 左右而提示处理PP却表现正常并给出经实测验证的修复方案关闭-fmoe或避免跨设备拆分 fused 张量对以及-fmoe的正确使用建议。读完本文你将掌握 MoE 大模型以 DeepSeek 系列为例在多卡环境下做细粒度张量卸载时的性能诊断方法以及 fused MoE 优化的适用边界与注意事项。一、问题背景一场 7 卡混跑的 TG 性能灾难1.1 复现环境问题由用户Panchovix在一套异构 GPU 集群上复现硬件环境如下CPURyzen 7 7800X3D内存192GB操作系统Fedora 41GPU共 7 张通过./llama-server --list-devices列出ggml_cuda_init: found 7 CUDA devices: Device 0: NVIDIA GeForce RTX 5090, compute capability 12.0, VMM: yes Device 1: NVIDIA GeForce RTX 4090, compute capability 8.9, VMM: yes Device 2: NVIDIA GeForce RTX 4090, compute capability 8.9, VMM: yes Device 3: NVIDIA GeForce RTX 5090, compute capability 12.0, VMM: yes Device 4: NVIDIA GeForce RTX 3090, compute capability 8.6, VMM: yes Device 5: NVIDIA GeForce RTX 3090, compute capability 8.6, VMM: yes Device 6: NVIDIA RTX A6000, compute capability 8.6, VMM: yes测试模型为 DeepSeek-R1-0528 的 UD 量化版本DeepSeek-R1-0528-UD-Q3_K_XL分片 GGUF共 7 个分片运行于 ik_llama.cpp。1.2 症状描述使用llama-sweep-bench做基准测试时配置了非常复杂的逐层逐张量卸载规则前 35 个 decoder 层的 FFN 权重被拆到 7 张不同的 GPU 上第 35、36 层则被拆成半层——即ffn_norm、ffn_gate_inp、ffn_gate_shexp、ffn_down_shexp、ffn_up_shexp以及ffn_gate_exps等张量被分散到 CUDA4 / CUDA5 两张卡上剩余 FFN 权重留在 CPU。结果非常反直觉PP提示处理速度正常约 150 t/s但 TG文本生成速度暴跌到约 1.5 t/s——对于一个 671B 参数的 MoE 模型来说这个数字连能用都算不上。而在同样的命令下主线的 llama.cppllama-serverTG 却能保持 7 t/s 以上尽管用户表示主线无法完整复现完全相同的 sweep-bench 设置只能加载 server 独立测试。1.3 第一组对照实验用户分别在 ik_llama.cpp 与主线 llama.cpp 上跑了几乎相同的命令差异仅在于 ik 侧额外带了-mla 1与-fmoeik_llama.cppllama-sweep-bench实测结果main: n_kv_max 16384, n_batch 2048, n_ubatch 2048, flash_attn 1, n_gpu_layers 999, n_threads 8, n_threads_batch 8 | PP | TG | N_KV | T_PP s | S_PP t/s | T_TG s | S_TG t/s | |-------|--------|--------|----------|----------|----------|----------| | 2048 | 512 | 0 | 13.489 | 151.83 | 326.413 | 1.57 | | 2048 | 512 | 2048 | 12.965 | 157.96 | 326.891 | 1.57 | | 2048 | 512 | 4096 | 13.751 | 148.93 | 327.513 | 1.56 | | 2048 | 512 | 6144 | 14.467 | 141.56 | 328.236 | 1.56 | | 2048 | 512 | 8192 | 15.263 | 134.18 | 329.009 | 1.56 |TG 稳定在 1.561.57 t/s且几乎不随 KV 缓存增长而变化——这强烈暗示瓶颈不在 KV 缓存访问而是每一步解码都卡在某个固定的串行环节上。主线 llama.cppllama-server实测结果抽取关键时序prompt eval time 9258.01 ms / 740 tokens ( 12.51 ms per token, 79.93 tokens per second) eval time 155399.04 ms / 1138 tokens ( 136.55 ms per token, 7.32 tokens per second) prompt eval time 12816.60 ms / 2036 tokens ( 6.29 ms per token, 158.86 tokens per second) eval time 179147.95 ms / 1299 tokens ( 137.91 ms per token, 7.25 tokens per second) prompt eval time 21481.40 ms / 2169 tokens ( 9.90 ms per token, 100.97 tokens per second) eval time 266511.08 ms / 1903 tokens ( 140.05 ms per token, 7.14 tokens per second) prompt eval time 24427.19 ms / 2751 tokens ( 8.88 ms per token, 112.62 tokens per second) eval time 281851.24 ms / 1996 tokens ( 141.21 ms per token, 7.08 tokens per second)主线在完全相同不含-fmoe/-mla的卸载方案下PP 约 80160 t/sTG 约 7 t/s。也就是说同样的半层拆分在主线不致命但在开启-fmoe的 ik_llama.cpp 上 TG 性能直接雪崩。1.4 修复对照整层卸载一切正常用户随后改用整层卸载把第 2023 层的 FFN 整块放到 CUDA4第 2426 层整块放到 CUDA5不再做层内半层拆分./llama-server -m /models_llm/DeepSeek-R1-0528-UD-Q3_K_XL-00001-of-00007.gguf -c 32768 --no-mmap -ngl 999 \ -ot blk.(0|1|2|3|4|5|6|7).ffn.CUDA0 -ot blk.(8|9|10|11).ffn.CUDA1 \ -ot blk.(12|13|14|15).ffn.CUDA2 -ot blk.(16|17|18|19|20).ffn.CUDA3 \ -ot blk.(20|21|22|23).ffn.CUDA4 -ot blk.(24|25|26).ffn.CUDA5 \ -ot blk.(27|28|29|30|31|32|33|34).ffn.CUDA6 -ot ffn.*CPU -fa -mg 0 -ub 2048 -mla 1 -fmoe结果 TG 恢复预期速度。由此可以确认问题出在第 35/36 层的半层层内张量拆分与-fmoe的交互上而不是整体的多卡拓扑。二、破案关键-fmoe 与张量跨设备拆分的冲突2.1 社区线索up/gate 必须同卡down 可以另放社区成员Ph0rk0z第一时间给出了方向性判断如果启用了-fmoe部分层的 up/gate 权重会被融合fused成一个算子在张量级卸载场景下up/gate 必须放在同一张卡上down 可以单独放另一张卡。否则推理图会在多张卡之间反复弹跳形成严重的跨设备同步瓶颈——这也解释了用户提到的PCIe 只有 X4跨卡带宽极差的加剧效应。Ph0rk0z还提到两个可观察的判据当 TG 卡死时往往只有 2 张 GPU 长期保持 50% 占用率其余 GPU 基本闲置整个生成期间持续如此GPU 利用率飙升的本质是在两卡之间来回弹跳bounce between 2 GPUs造成瓶颈。2.2 实测验证关闭 -fmoe 立即恢复用户按此建议在保持原半层拆分不变的前提下仅去掉-fmoeTG 性能立即恢复| PP | TG | N_KV | T_PP s | S_PP t/s | T_TG s | S_TG t/s | |-------|--------|--------|----------|----------|----------|----------| | 2048 | 512 | 0 | 10.990 | 186.35 | 58.726 | 8.72 | | 2048 | 512 | 2048 | 10.805 | 189.53 | 59.120 | 8.66 | | 2048 | 512 | 4096 | 11.567 | 177.05 | 59.698 | 8.58 | | 2048 | 512 | 6144 | 12.275 | 166.84 | 60.586 | 8.45 |对比表配置S_PP t/sN_KV0S_TG t/sN_KV0半层拆分 -fmoeik151.831.57半层拆分主线无-fmoe~80–160~7.1–7.3半层拆分 关闭-fmoeik186.358.72关闭-fmoe后不仅 TG 从 1.57 t/s 恢复到 8.72 t/sPP 也进一步从 151.83 提升到 186.35 t/s。这说明当张量被跨设备拆分时fused MoE 非但不是加速反而成了性能杀手。三、源码级原理解析-ot 与 -fmoe 到底做了什么3.1 -ot / --override-tensor张量级缓冲类型覆盖-ot是 ik_llama.cpp 提供的按张量名覆盖缓冲buffer类型的参数允许把任意张量显式放置到指定后端设备如CUDA0…CUDA7、CPU。其解析逻辑位于 common/common.cppif (arg -ot || arg --override-tensor) { CHECK_ARG if (!parse_buft_overrides(std::string{ argv[i] }, params.tensor_buft_overrides)) { fprintf(stderr, error: Invalid tensor buffer type override: %s\n, argv[i]); invalid_param true; } return true; }核心函数parse_buft_overridescommon/common.cpp会先枚举当前注册的所有后端 buffer 类型ggml_backend_reg_get_default_buffer_type构造一个名字 - buft的映射表随后按逗号拆分tensor_namebuft键值对逐一校验两侧的合法性最终生成llama_model_tensor_buft_override列表。如果右侧的名字不在映射表中会打印出所有可用 buffer 类型并返回错误。关键语法点支持正则表达式张量名部分可以是正则如blk.(0|1|2|...).ffn.*因此-ot blk.35.ffn_gate_exps.weightCUDA4可以精确命中某一层的某个具体张量可多次指定每条-ot只声明一条规则多条规则按命令行顺序叠加从而构造出上文中那种逐层逐张量的复杂拓扑覆盖粒度是张量这是与-ngl按层数卸载最本质的区别——-ot可以在同一层内把不同张量分配到不同设备即半层/层内拆分。3.2 -fmoe / -no-fmoefused MoE 优化开关-fmoe--fused-moe默认开启对应参数params.fused_moe_up_gate-no-fmoe--no-fused-moe用于显式关闭common/common.cppif (arg -no-fmoe || arg --no-fused-moe) { params.fused_moe_up_gate false; return true; }该参数最终传导到上下文参数cparams.fused_moe_up_gatesrc/llama-cparams.h并在加载时打印状态LLAMA_LOG_INFO(%s: fused_moe %d\n, __func__, cparams.fused_moe_up_gate); LLAMA_LOG_INFO(%s: fused_up_gate %d\n, __func__, cparams.fused_up_gate);见 src/llama.cpp默认值均为true见 src/llama.cpp。其底层行为可从 src/llama-build-context.cpp 的图构建逻辑看出端倪当can_use_fmoe cparams.fused_moe_up_gate up_exps-type gate_exps-type成立时构建器会调用ggml_moe_up_gate/ggml_moe_up_gate_ext将up 专家矩阵与 gate 专家矩阵的mul_mat_id合并成单个算子fused upgate否则退化为两次独立的ffn_moe_up与ffn_moe_gate计算再用ggml_fused_mul_unary做 SiLU/GELU 门控融合src/llama-build-context.cpp。注意即使关闭-fmoefused_up_gateup/gate 激活门控融合仍可能生效-no-fug--no-fused-up-gate可再单独关闭它。两者是不同层级的优化-fmoe融合的是跨专家expert的 up/gate 矩阵乘-fug融合的是单个 token 上的 up/gate 激活运算。由此可以推断性能雪崩的直接机理-fmoe把ffn_up_exps与ffn_gate_exps视为必须在同一设备上成对参与同一个融合算子的输入而-ot的半层拆分把它们分别放到了 CUDA4 / CUDA5 两张不同的卡甚至与ffn.*CPU规则叠加后部分落到 CPU每次解码步骤中融合算子需要跨设备搬运/同步up_exps与gate_exps的数据而用户机器上 GPU 间仅 PCIe x4 连接结果每一步 token 生成都被串行地阻塞在跨卡数据传输上TG 从 ~8 t/s 雪崩到 ~1.5 t/s而 PP 阶段由于 batch 大、可并行掩盖延迟看起来仍正常。这正与 issue 中PP 正常、TG 崩盘的现象吻合也与Ph0rk0z观察到的TG 期间少数 GPU 长期高占用、其余闲置的 GPU 利用率特征一致。3.3 张量命名约定DeepSeek 的 ffn 权重族为了写出正确的-ot规则需要知道 DeepSeek 系列 MoE 层的 FFN 张量命名。从 src/graphs/build_deepseek2.cpp 和 src/graphs/build_deepseek4.cpp 可以看到构建时使用的一族张量ffn_normFFN 输入归一化权重ffn_gate_inp门控router输入权重ffn_gate_exps门控专家权重与 up 融合的候选者ffn_up_expsup 专家权重ffn_down_expsdown 专家权重ffn_gate_shexp/ffn_up_shexp/ffn_down_shexp共享专家shared expert的门控 / up / down 权重issue 中-ot blk.35.ffn_(norm|gate_inp|gate_shexp|down_shexp|up_shexp).weightCUDA4一类规则正是用正则一次性命中某层的这组小张量再单独用-ot blk.35.ffn_gate_exps.weightCUDA4处理专家矩阵。正是这种把ffn_gate_exps与潜在的融合对象分开对待的写法与-fmoe的融合逻辑产生了冲突。四、实战排查方法论如何定位多卡 TG 瓶颈4.1 用 llama-sweep-bench 复现并量化问题ik_llama.cpp 自带的 examples/sweep-bench源码位于 examples/sweep-bench/sweep-bench.cpp构建目标见 examples/sweep-bench/CMakeLists.txt专为观察性能随上下文长度变化而设计它在每个 ubatch 窗口内依次执行生成 ubatch/4 个 token → 测生成速度 → 清 KV → 处理一批随机 token → 测提示处理速度从而给出 PP/TG 随N_KV递增的逐窗口指标而不是把整个上下文平均成一个数。sweep-bench 输出列的含义摘自其 README列含义PP每个 ubatch 处理的提示 token 数TG每个 ubatch 生成的 token 数N_KV当前 KV 缓存大小T_PP提示处理耗时即 TTFT 相关S_PP提示处理速度(B*PP)/T_PP或PP/T_PPT_TG生成全部 batch 的总耗时S_TG文本生成速度(B*TG)/T_TG同时支持--output-format jsonl输出机器可读的 JSONL每个窗口一条记录字段与上表一一对应便于脚本化对比不同配置。最小复现命令来自 README 示例./llama-sweep-bench -c 8704 -ub 512 -m models/Meta-Llama-3.2-3B-Instruct-Q8_0.ggufissue 中实际使用的完整命令还叠加了多卡卸载、-faflash attention、-mg 0主 GPU 为 0 号、-ub 2048微批 2048、-mla 1MLA 路径与-fmoe。4.2 二分法对比实验设计issue 的排查过程本身就是一套可复用的控制变量方法基线整层卸载-ot只按层拆分blk.N.ffn.CUDAx确认无半层拆分时 TG 正常加入半层拆分把个别层的ffn_gate_exps与其余 FFN 张量分开blk.35/36分别到 CUDA4/CUDA5复现 TG 雪崩同一拓扑、不同实现主线 llama.cpp 无-fmoe时 TG 约 7 t/s证明拓扑本身不是致命问题同一实现、切换开关ik 侧关闭-fmoe后 TG 恢复到 8.72 t/s锁定根因是 fused MoE 与跨设备拆分冲突。这套方法的核心启示当 TG 性能异常而 PP 正常时优先怀疑解码路径上新增的跨设备同步点对 MoE 模型先检查-fmoe融合的张量对是否被-ot拆到了不同设备。4.3 辅助判据GPU 利用率用户补充的观测进一步佐证了瓶颈位置PP 阶段主 GPU-mg 0接近 100% 占用正常TG 阶段开始时若干 GPU 各约 90%随后跌到 1030% 并持续——说明大部分时间 GPU 在等待跨卡/跨设备数据而不是在计算Ph0rk0z描述的仅 2 张 GPU 50% 占用持续整个生成过程是另一类典型症状融合算子被迫在两卡之间反复弹跳。注意用户指出其 PCIe 为 x4跨 GPU 带宽很差这放大了跨卡搬运的代价若 PCIe 带宽充足如 x16 或 NVLink症状可能相对缓和但 fused 张量对跨设备的逻辑冲突依然存在。五、结论与最佳实践5.1 明确结论在 ik_llama.cpp 中-ot提供的张量级卸载与默认开启的-fmoefused MoE存在已知的冲突场景当 up/gate 专家权重被-ot分散到不同 GPU或 GPU 与 CPU 之间时-fmoe反而会让 TG 性能雪崩本案例从 ~8.7 t/s 跌到 ~1.5 t/s而 PP 基本不受影响。5.2 操作建议MoE 模型强烈建议保留-fmoe社区成员ubergarm在 DeepSeek-TNG-R1T2-Chimera-GGUF 讨论中的总结-fmoe对任何 MoE包括 DeepSeek 系列都是重要的计算优化不要用-ot把ffn_gate_expsgate/up 专家权重拆分到不同 GPU/CPU——up/gate 必须同卡才能让 fused 算子完整生效down 专家ffn_down_exps与 up/gate 不参与同一融合算子可以视容量放到另一张卡。若确实需要层内半层拆分例如显存不足以整层落卡则必须显式关闭 fused MoE./llama-server -m model.gguf -ngl 999 -fmoe 0 -ot ...半层拆分规则或使用等价的-no-fmoe。代价是失去 fused MoE 的算子级优化但可以换取跨设备拓扑的正确性。验证手段修改拓扑或开关后用llama-sweep-bench或--output-format jsonl在相同-c/-ub下对比S_TG随N_KV的变化曲线同时用nvidia-smi观察 TG 阶段的 GPU 利用率分布二者结合可快速判断是否又出现跨卡同步瓶颈。关于主线对比的说明issue 中主线 llama.cpp 未启用-fmoe/-mla且用户无法以完全相同的 sweep-bench 参数加载只能以 server 独立测试近似对比因此主线 7 t/s 与 ik 侧数值并非严格同口径但半层拆分在 ik -fmoe下 TG 雪崩的结论由 ik 侧自身的开关对照实验-fmoevs-no-fmoe即可独立验证。其它相关开关-no-fug--no-fused-up-gate可单独关闭 up/gate 激活融合-no-mmad--no-fused-mul-multiadd可关闭 mul-multiadd 融合common/common.cpp。在多卡/异构拓扑下做张量级卸载时如仍遇异常可依次关闭这些融合选项做二分定位。5.3 一句话总结-ot让你能把模型拆到任意设备-fmoe则假设 up/gate 专家矩阵始终成对出现、同地计算——两者叠加时必须保证融合张量对不跨设备否则 TG 性能会以数量级的形式偿还这笔拆分之债。【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考