ik_llama.cpp 多 GPU 推理调优:深入解析 GGML_SCHED_MAX_COPIES 与 pipeline parallelism 的 VRAM 权衡
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
导读
GGML_SCHED_MAX_COPIES 是 ggml 调度器(backend scheduler)中控制 pipeline parallelism 输入副本数的编译期宏,它直接决定多 GPU 推理时的显存(VRAM)占用与吞吐表现。本文以 ik_llama.cpp 仓库讨论 #100("New argument / env variable for GGML_SCHED_MAX_COPIES?")为核心线索,从源码层面剖析该宏的启用条件、默认值与影响机制,并结合仓库内大量多 GPU 用户的实测记录,给出"何时该用 1、何时可以调高、如何验证生效"的完整实战方案。
讨论的起点:一个关于"能否不重编译就改宏"的需求
2024-10-21,用户 Nexesenex 在 discussion #100 中向项目作者 ikawrakow 提出请求:能否提供一个 CLI 参数(或至少一个环境变量)来设置GGML_SCHED_MAX_COPIES,从而避免每次调整都要重新编译。理由是:
- 该宏影响 VRAM 占用与性能;
- 用户希望在不重编译的前提下方便地做基准测试(benching)和定制化使用。
作者的第一反应是"我还没研究过这个,它到底有什么用?"。Nexesenex 随即给出了自己的观察:它本意是加速多 GPU 推理;主流 llama.cpp 的默认值是 4,而他自己设成 1,因为当初没有观察到明显提升,反而注意到更高的 VRAM 消耗和 GPU 负载。
这个讨论虽然没有最终落地成运行时参数,但它引出了一个贯穿 ik_llama.cpp 多 GPU 生态的关键实践:GGML_SCHED_MAX_COPIES 的值必须在编译期确定,而它的选择会显著影响显存分配与推理行为。后文将结合当前仓库源码与大量后续社区记录,完整还原这一机制。
GGML_SCHED_MAX_COPIES 在源码中的定位
编译期宏,而非运行时开关
GGML_SCHED_MAX_COPIES 是编译期宏,通过 CMake 缓存变量注入,最终以-D编译定义的形式传给所有 ggml 源文件:
顶层缓存变量定义于 ggml/CMakeLists.txt:
set(GGML_SCHED_MAX_COPIES "1" CACHE STRING "ggml: max input copies for pipeline parallelism")通过 ggml/src/CMakeLists.txt 写入编译定义:
add_compile_definitions(GGML_SCHED_MAX_COPIES=${GGML_SCHED_MAX_COPIES})在 ggml/src/ggml-backend.cpp 中,代码层面还有一层默认值保护:
#ifndef GGML_SCHED_MAX_COPIES #define GGML_SCHED_MAX_COPIES 1 #endif
从源码结构可以推断:由于该值参与数组维度声明和图形副本分配,它必须是一个编译期常量,因此无法在运行时通过环境变量或 CLI 参数直接修改——这正是 discussion #100 中需求的本质障碍。当前仓库中 ik_llama.cpp 的默认值已经是1,与讨论发生时主流 llama.cpp 的默认值4不同,这一点在后文会详细对比。
该宏在调度器中扮演的角色
在ggml_backend_sched结构体中,pipeline parallelism 支持被明确标注(ggml/src/ggml-backend.cpp):
// pipeline parallelism support int n_copies; int cur_copy; ggml_backend_event_t events[GGML_SCHED_MAX_BACKENDS][GGML_SCHED_MAX_COPIES]; struct ggml_tensor * graph_inputs[GGML_SCHED_MAX_SPLIT_INPUTS]; int n_graph_inputs;关键点在于events数组的第二维直接以GGML_SCHED_MAX_COPIES作为维度大小,且张量副本表hv_tensor_copies的分配也与n_copies成正比(ggml/src/ggml-backend.cpp):
sched->n_copies = parallel ? GGML_SCHED_MAX_COPIES : 1; ... sched->hv_tensor_copies = (ggml_tensor **)malloc(sched->hash_set.size * sched->n_backends * sched->n_copies * sizeof(struct ggml_tensor *));也就是说:只有启用了 pipeline parallelism(parallel == true)时,GGML_SCHED_MAX_COPIES才会生效;否则n_copies恒为 1。
在图形分割阶段(ggml/src/ggml-backend.cpp),调度器会为每个后端上的输入张量按n_copies创建多份副本,并将这些副本标记为 input/output,防止 ggml-alloc 覆盖它们。这些副本会占用额外的设备端(GPU)与主机端(CUDA_Host)内存,这就是"VRAM 占用上升"的直接来源。
pipeline parallelism 何时被启用:llama.cpp 层的条件
真正决定parallel取值的逻辑位于 src/llama.cpp:
// enabling pipeline parallelism in the scheduler increases memory usage, so it is only done when necessary bool pipeline_parallel = llama_get_device_count(*model) > 1 && model->n_gpu_layers > (int)model->hparams.n_layer && model->split_mode == LLAMA_SPLIT_MODE_LAYER && params.offload_kqv && !model->has_tensor_overrides(); #ifndef GGML_USE_CUDA // pipeline parallelism requires support for async compute and events // currently this is only implemented in the CUDA backend pipeline_parallel = false; #endif ctx->sched = ggml_backend_sched_new(ctx->backends.data(), backend_buft.data(), ctx->backends.size(), max_nodes, pipeline_parallel); if (pipeline_parallel) { LLAMA_LOG_INFO("%s: pipeline parallelism enabled (n_copies=%d)\n", __func__, ggml_backend_sched_get_n_copies(ctx->sched)); }从这段代码可以归纳出 pipeline parallelism 的同时成立条件:
- 模型分布在多个设备上(
llama_get_device_count(*model) > 1); - 模型被全量 offload 到 GPU(
n_gpu_layers > n_layer); - 采用按层切分模式(
LLAMA_SPLIT_MODE_LAYER,即默认的-sm layer); - 允许 KV 缓存 offload(
params.offload_kqv,即-ov相关参数); - 未使用张量级覆盖(
!model->has_tensor_overrides(),即未使用-ot做精细 offload); - 后端为 CUDA(非 CUDA 后端一律关闭)。
值得注意的注释:"enabling pipeline parallelism in the scheduler increases memory usage, so it is only done when necessary"——即启用该机制本身就会增加内存占用,所以仅在上述条件全部满足时才开启。
如果启用了 pipeline parallelism,启动日志中会出现:
llama_new_context_with_model: pipeline parallelism enabled (n_copies=1)这条日志(n_copies的值)就是验证编译配置是否生效的最直接依据。
一个已知的"误启用"场景:tensor override
在 issue #437 中,社区成员 saood06 分析了为何很多用户会遇到"异常巨大的显存分配":pipeline parallelism 的启用条件里使用的是model->n_gpu_layers > (int)model->hparams.n_layer这一假设,而一旦使用-ot(tensor override)精细指定张量落盘位置,这个"全量 offload"的假设就不再成立,但检查逻辑并未感知到这一点,从而可能在不该启用时仍然启用。issue #500 中作者 ikawrakow 也确认"默认值 4 在某些情况下会导致异常的内存分配(insane memory allocations),已经有多人遇到同样问题",并建议用户先尝试-DGGML_SCHED_MAX_COPIES=1。
为什么默认值 4 会让多 GPU 用户"显存失控"
讨论 #100 中用户报告"mainline 默认 4,我设 1 后注意到更多 VRAM 消耗和 GPU 负载";后续大量 issue/discussion 给出了具体数字。这里选取仓库内可查证的实例:
| 场景 | n_copies | 表现 |
|---|---|---|
| issue #500(双 3090) | 4 | allocating 360757.13 MiB直接 OOM |
| issue #437 | 4 | 尝试分配1130415.93 MiB(约 1.1 TB),cudaMalloc failed: out of memory |
| discussion #384(双 2080Ti 老工作站) | 4 | 尝试分配167771.94 MiB,OOM;改用 1 后恢复正常,约 15 tok/s PP、6 tok/s TG |
| discussion #258 | 4→1 | 用户报告"rebuilding with-DGGML_SCHED_MAX_COPIES=1really reduced VRAM usage" |
| PR #237(16 卡示例) | 4 | 每张卡 compute buffer 高达5088.02 MiB,且各卡完全一致 |
从以上案例可以推断:当n_copies > 1时,调度器为每个后端创建的输入副本、事件对象和计算缓冲区都会成倍增长;在多卡 + 大模型(尤其是 DeepSeek/Qwen3 这类大规模 MoE)组合下,compute buffer 的膨胀可能达到几十 GB 甚至 TB 级,最终触发cudaMalloc failed: out of memory。
实战:如何把 GGML_SCHED_MAX_COPIES 设成 1(及验证方法)
1. 编译时指定
当前仓库默认值已经是 1,但显式指定可以避免"默认值依赖",也可在后续升级中保持行为一致:
cmake -B ./build -DGGML_CUDA=ON -DGGML_BLAS=OFF -DGGML_SCHED_MAX_COPIES=1 cmake --build ./build --config Release -j $(nproc)该选项也出现在官方 Windows 构建文档 docs/build.md 中,可作为标准构建参数之一。仓库内大量多 GPU 用户的推荐组合(如 discussion #477、discussion #532、issue #576)大致是:
cmake -B ./build -DCMAKE_BUILD_TYPE=Release \ -DGGML_CUDA=ON \ -DGGML_RPC=OFF \ -DGGML_BLAS=OFF \ -DGGML_SCHED_MAX_COPIES=1 \ -DGGML_CUDA_IQK_FORCE_BF16=1 cmake --build ./build --config Release -j $(nproc)说明:
-DGGML_CUDA_IQK_FORCE_BF16=1与GGML_SCHED_MAX_COPIES无直接关系,它用于让部分无 MMQ 内核的量化在 CUDA 上使用 bf16 cuBLAS,是多 GPU 跑 DeepSeek 等模型时的常见配套项(见 ggml/CMakeLists.txt)。是否加入取决于你的模型与显卡组合。
2. 验证是否生效
运行任意可执行文件(如llama-server、llama-cli、llama-bench),观察启动日志:
- 若输出
pipeline parallelism enabled (n_copies=1),说明以 1 份副本运行; - 若输出
pipeline parallelism enabled (n_copies=4),说明构建时该宏仍为 4(或该宏未被覆盖); - 若不出现该日志,说明当前运行配置未满足启用条件(例如单 GPU、未全量 offload、使用
-ot、或非 CUDA 后端),此时该宏不生效。
同时对照 compute buffer 数值,例如 PR #492 中 n_copies=1 时的典型输出:
llama_new_context_with_model: pipeline parallelism enabled (n_copies=1) llama_new_context_with_model: CUDA0 compute buffer size = 2094.00 MiB llama_new_context_with_model: CUDA1 compute buffer size = 2125.00 MiB llama_new_context_with_model: CUDA_Host compute buffer size = 932.00 MiB3. 与相关运行参数的配合
从 discussion #258 中社区总结的经验来看:
- 编译使用
-DGGML_SCHED_MAX_COPIES=1后,多 GPU 显存分配更符合预期; - 释放出的显存可以用于增大
--batch-size/--ubatch-size(例如-b 4096 -ub 4096),通过更大批量换取更高的 prefill(PP)吞吐(见 issue #425 中作者的回复); - 若仍需要更细粒度的显存控制,可以配合
-ot(tensor override)把部分层或专家张量留在 CPU,但要意识到-ot会破坏 pipeline parallelism 的启用假设(见 issue #437 的分析)。
权衡:n_copies=1 与更高值的取舍
为什么有人愿意用更高的 n_copies
讨论 #100 中提到"它本意是加速多 GPU 推理"。从实现看,pipeline parallelism 的目标是让不同后端(多张 GPU)的计算与拷贝尽量重叠:调度器为输入张量维护多份副本并借助后端事件(events)做流水线同步(ggml/src/ggml-backend.cpp),这在理想情况下能提升吞吐。这也是主流 llama.cpp 长期默认4的原因。
为什么多数 ik_llama.cpp 多 GPU 用户选择 1
社区在 discussion #459 中的共识是:默认 4 会让多 GPU 用户分配出远超预期的显存;设为 1 后显存占用符合预期、更简单直观,然后通过加大 batch 来提速。另有用户指出,由于大多数人已经用-ot手动决定各张量落在哪张卡上,pipeline 副本带来的收益变得可有可无("Exact same results as taking a single layer off")。
综合仓库证据,可以给出如下决策建议:
- 多 GPU 跑大模型、且显存紧张:优先
1,这是当前仓库默认值,也是绝大多数实战记录的选择; - 追求极致吞吐、显存充裕、且未使用
-ot:可以尝试更高值(如 4),并对照pipeline parallelism enabled (n_copies=4)日志与 compute buffer 大小做基准对比,观察 PP/TG 的实际变化; - 任何"显存异常暴涨 / 莫名 OOM":先确认构建时该宏是否为 1,再排查是否误触发了 pipeline parallelism 的启用条件。
总结
回到 discussion #100 的原始诉求:GGML_SCHED_MAX_COPIES 本质是编译期宏,受限于数组维度和副本分配机制,无法在运行时用 CLI 参数或环境变量调整,因此在当前 ik_llama.cpp 中,改它必须重编译。但仓库演进给出了更优解:ik_llama.cpp 已将默认值从主流的 4 改为 1(ggml/CMakeLists.txt),从源头规避了多 GPU 用户最常见的显存失控问题。
对开发者而言,本主题的可操作结论是:
- 构建时显式传入
-DGGML_SCHED_MAX_COPIES=1,保证多 GPU 显存分配可控; - 用启动日志中的
pipeline parallelism enabled (n_copies=...)验证编译配置与运行路径; - 理解 pipeline parallelism 的启用条件(多设备、全量 offload、layer split、offload_kqv、无 tensor override、CUDA 后端),避免在错误场景下依赖或误解该宏;
- 在显存允许的前提下,用更大的 batch/ubatch 换取 PP 吞吐,而不是盲目调高 n_copies。
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考