ik_llama.cpp 多 GPU 混合卸载调优实战:-ot 半层拆分下 TG 性能骤降的根因分析与 -fmoe 的正确用法
2026/9/19 3:36:00 网站建设 项目流程

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 集群上复现,硬件环境如下:

  • CPU:Ryzen 7 7800X3D
  • 内存:192GB
  • 操作系统:Fedora 41
  • GPU(共 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_normffn_gate_inpffn_gate_shexpffn_down_shexpffn_up_shexp以及ffn_gate_exps等张量被分散到 CUDA4 / CUDA5 两张卡上,剩余 FFN 权重留在 CPU。

结果非常反直觉:PP(提示处理)速度正常(约 150 t/s),但 TG(文本生成)速度暴跌到约 1.5 t/s——对于一个 671B 参数的 MoE 模型来说,这个数字连"能用"都算不上。而在同样的命令下,主线的 llama.cpp(llama-server)TG 却能保持 7 t/s 以上(尽管用户表示主线无法完整复现完全相同的 sweep-bench 设置,只能加载 server 独立测试)。

1.3 第一组对照实验

用户分别在 ik_llama.cpp 与主线 llama.cpp 上跑了几乎相同的命令,差异仅在于 ik 侧额外带了-mla 1-fmoe

ik_llama.cpp(llama-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.56~1.57 t/s,且几乎不随 KV 缓存增长而变化——这强烈暗示瓶颈不在 KV 缓存访问,而是每一步解码都卡在某个固定的串行环节上。

主线 llama.cpp(llama-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 约 80~160 t/s,TG 约 7 t/s。也就是说:同样的"半层拆分"在主线不致命,但在开启-fmoe的 ik_llama.cpp 上 TG 性能直接雪崩

1.4 修复对照:整层卸载一切正常

用户随后改用"整层"卸载(把第 20~23 层的 FFN 整块放到 CUDA4,第 24~26 层整块放到 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 立即恢复

用户按此建议,在保持原半层拆分不变的前提下仅去掉-fmoe,TG 性能立即恢复:

| 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/s(N_KV=0)S_TG t/s(N_KV=0)
半层拆分 +-fmoe(ik)151.831.57
半层拆分(主线,无-fmoe~80–160~7.1–7.3
半层拆分 + 关闭-fmoe(ik)186.358.72

关闭-fmoe后,不仅 TG 从 1.57 t/s 恢复到 8.72 t/s,PP 也进一步从 151.83 提升到 186.35 t/s。这说明:当张量被跨设备拆分时,fused MoE 非但不是加速,反而成了性能杀手

三、源码级原理解析:-ot 与 -fmoe 到底做了什么

3.1 -ot / --override-tensor:张量级缓冲类型覆盖

-ot是 ik_llama.cpp 提供的按张量名覆盖缓冲(buffer)类型的参数,允许把任意张量显式放置到指定后端设备(如CUDA0CUDA7CPU)。其解析逻辑位于 common/common.cpp:

if (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_overrides(common/common.cpp)会先枚举当前注册的所有后端 buffer 类型(ggml_backend_reg_get_default_buffer_type),构造一个名字 -> buft的映射表;随后按逗号拆分tensor_name=buft键值对,逐一校验=两侧的合法性,最终生成llama_model_tensor_buft_override列表。如果=右侧的名字不在映射表中,会打印出所有可用 buffer 类型并返回错误。

关键语法点:

  • 支持正则表达式:张量名部分可以是正则,如blk.(0|1|2|...).ffn.*,因此-ot "blk.35.ffn_gate_exps.weight=CUDA4"可以精确命中某一层的某个具体张量;
  • 可多次指定:每条-ot只声明一条规则,多条规则按命令行顺序叠加,从而构造出上文中那种逐层逐张量的复杂拓扑;
  • 覆盖粒度是张量:这是与-ngl(按层数卸载)最本质的区别——-ot可以在同一层内把不同张量分配到不同设备(即"半层/层内拆分")。

3.2 -fmoe / -no-fmoe:fused MoE 优化开关

-fmoe--fused-moe)默认开启,对应参数params.fused_moe_up_gate-no-fmoe--no-fused-moe)用于显式关闭(common/common.cpp):

if (arg == "-no-fmoe" || arg == "--no-fused-moe") { params.fused_moe_up_gate = false; return true; }

该参数最终传导到上下文参数cparams.fused_moe_up_gate(src/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 up+gate");否则退化为两次独立的ffn_moe_upffn_moe_gate计算,再用ggml_fused_mul_unary做 SiLU/GELU 门控融合(src/llama-build-context.cpp)。

注意:即使关闭-fmoefused_up_gate(up/gate 激活门控融合)仍可能生效;-no-fug--no-fused-up-gate)可再单独关闭它。两者是不同层级的优化:-fmoe融合的是跨专家(expert)的 up/gate 矩阵乘-fug融合的是单个 token 上的 up/gate 激活运算

由此可以推断性能雪崩的直接机理:

  1. -fmoeffn_up_expsffn_gate_exps视为必须在同一设备上成对参与同一个融合算子的输入;
  2. -ot的半层拆分把它们分别放到了 CUDA4 / CUDA5 两张不同的卡(甚至与ffn.*=CPU规则叠加后部分落到 CPU);
  3. 每次解码步骤中,融合算子需要跨设备搬运/同步up_expsgate_exps的数据,而用户机器上 GPU 间仅 PCIe x4 连接;
  4. 结果每一步 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_norm:FFN 输入归一化权重
  • ffn_gate_inp:门控(router)输入权重
  • ffn_gate_exps:门控专家权重(与 up 融合的候选者)
  • ffn_up_exps:up 专家权重
  • ffn_down_exps:down 专家权重
  • 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).weight=CUDA4"一类规则正是用正则一次性命中某层的这组"小张量",再单独用-ot "blk.35.ffn_gate_exps.weight=CUDA4"处理专家矩阵。正是这种把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_PPPP/T_PP
T_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.gguf

issue 中实际使用的完整命令还叠加了多卡卸载、-fa(flash attention)、-mg 0(主 GPU 为 0 号)、-ub 2048(微批 2048)、-mla 1(MLA 路径)与-fmoe

4.2 二分法对比实验设计

issue 的排查过程本身就是一套可复用的"控制变量"方法:

  1. 基线整层卸载-ot只按层拆分(blk.N.ffn.=CUDAx),确认"无半层拆分"时 TG 正常;
  2. 加入半层拆分:把个别层的ffn_gate_exps与其余 FFN 张量分开(blk.35/36分别到 CUDA4/CUDA5),复现 TG 雪崩;
  3. 同一拓扑、不同实现:主线 llama.cpp 无-fmoe时 TG 约 7 t/s,证明拓扑本身不是致命问题;
  4. 同一实现、切换开关: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%,随后跌到 10~30% 并持续——说明大部分时间 GPU 在等待跨卡/跨设备数据,而不是在计算;
  • Ph0rk0z描述的"仅 2 张 GPU >50% 占用持续整个生成过程"是另一类典型症状:融合算子被迫在两卡之间反复弹跳。

注意:用户指出其 PCIe 为 x4,跨 GPU 带宽很差,这放大了跨卡搬运的代价;若 PCIe 带宽充足(如 x16 或 NVLink),症状可能相对缓和,但 fused 张量对跨设备的逻辑冲突依然存在。

五、结论与最佳实践

5.1 明确结论

在 ik_llama.cpp 中,-ot提供的张量级卸载与默认开启的-fmoe(fused MoE)存在已知的冲突场景:当 up/gate 专家权重被-ot分散到不同 GPU(或 GPU 与 CPU 之间)时,-fmoe反而会让 TG 性能雪崩(本案例从 ~8.7 t/s 跌到 ~1.5 t/s),而 PP 基本不受影响。

5.2 操作建议

  1. MoE 模型强烈建议保留-fmoe(社区成员ubergarm在 DeepSeek-TNG-R1T2-Chimera-GGUF 讨论中的总结):

    • -fmoe对任何 MoE(包括 DeepSeek 系列)都是重要的计算优化;
    • 不要用-otffn_gate_exps(gate/up 专家权重)拆分到不同 GPU/CPU——up/gate 必须同卡,才能让 fused 算子完整生效;
    • down 专家(ffn_down_exps)与 up/gate 不参与同一融合算子,可以视容量放到另一张卡。
  2. 若确实需要层内半层拆分(例如显存不足以整层落卡),则必须显式关闭 fused MoE:

./llama-server -m model.gguf -ngl 999 -fmoe 0 -ot ...(半层拆分规则)

或使用等价的-no-fmoe。代价是失去 fused MoE 的算子级优化,但可以换取跨设备拓扑的正确性。

  1. 验证手段:修改拓扑或开关后,用llama-sweep-bench(或--output-format jsonl)在相同-c/-ub下对比S_TGN_KV的变化曲线;同时用nvidia-smi观察 TG 阶段的 GPU 利用率分布,二者结合可快速判断是否又出现跨卡同步瓶颈。

  2. 关于主线对比的说明:issue 中主线 llama.cpp 未启用-fmoe/-mla,且用户无法以完全相同的 sweep-bench 参数加载,只能以 server 独立测试近似对比,因此主线 7 t/s 与 ik 侧数值并非严格同口径,但"半层拆分在 ik +-fmoe下 TG 雪崩"的结论由 ik 侧自身的开关对照实验(-fmoevs-no-fmoe)即可独立验证。

  3. 其它相关开关-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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询