Eval bug: Loading a draft model crashes with “invalid vector subscript” when the target model has filled the GPU (NaN in `splits` → `devices

这个报错通常出现在 llama.cpp 先加载已占满显存的目标模型、再加载 draft(草稿)模型做投机解码时;底层原因是显存空闲量读到 0,导致 splits 归一出 NaN, devices.at() 越界。优先排查目标模型是否已经把显卡显存吃满,并尝试用 -ts 1 绕过按空闲显存切分的代码路

快速结论:这个报错通常出现在 llama.cpp 先加载已占满显存的目标模型、再加载 draft(草稿)模型做投机解码时;底层原因是显存空闲量读到 0,导致 splits 归一出 NaN,devices.at() 越界。优先排查目标模型是否已经把显卡显存吃满,并尝试用 -ts 1 绕过按空闲显存切分的代码路径。

适用环境:Issue 已确认环境为 Windows(Windows 11 26200 亦有复现),llama.cpp CUDA 后端与 HIP/ROCm 后端均可复现,官方 release 构建与源码自编译均出现;涉及 NVIDIA GeForce RTX 4070 Ti 12 GB、RTX 5060 Ti 16 GB(sm_120)、AMD Radeon 780M(gfx1103,UMA)等单 GPU 场景,MSVC 编译器。触发组合包括 target + DFlash draft、target + MTP sidecar 等。

最快修复方案:给 draft 加载命令加上 --tensor-split 1(-ts 1):设置后 all_zero 为假,跳过按空闲显存切分的整段逻辑,splits[] 不会全零,也就不会产生 NaN。Issue 评论中已在 AMD 780M 上验证:原本必定崩溃的完整命令加上 -ts 1 后可正常加载并运行投机解码。

注意事项:-ts 1 是绕过而非根治,它改变了显存切分策略,是否适用于多卡或需要精细显存分配的场景未在 Issue 中验证。降低目标模型的 -ngl 在本机并不可靠(-ngl 20 仍崩溃)。真正的修复需要改 src/llama-model.cpp,即加 split_sum == 0 保护并 clamp layer_gpu,相关 PR(#27657 / #27647 / #27887)在 Issue 中仅被提及,未确认全部合并。

问题场景

使用 llama.cpp 的 llama-server 或 llama-cli 加载一个占满显存的目标模型后,再加载 draft 模型做投机解码(DFlash draft、MTP sidecar 等)。当 draft 开始加载时,目标模型已经把 GPU 显存占用到空闲量为 0,此时 llama_model::load_tensors 在加载 draft 权重阶段直接中止,报出 invalid vector subscript。

Issue 中给出的可复现命令形态(已脱敏模型路径):

llama-cli.exe -m <target>.gguf -ngl 20 -c 2048 -ub 64 -b 128 --no-host \
              -md <draft>.gguf --spec-type draft-dflash --spec-draft-n-max 4 -ngld 99 \
              --reasoning off

另一例(Windows 11 / RTX 5060 Ti,100% 复现):

llama-server -m Qwen3.8-27B-UD-IQ3_S.gguf --mmproj mmproj-F16.gguf --no-mmproj-offload \
  -ngl 99 -ngld 99 -fa on -c 65536 -ctk q4_0 -ctv q4_0 \
  -md <draft> --spec-type draft-dflash --spec-draft-n-max 8 --jinja --port 8081

报错原文

error loading model: invalid vector subscript

llama_model_load: error loading model: invalid vector subscript
llama_model_load_from_file_impl: failed to load model
common_speculative_init_result: failed to load draft model, '<draft>.gguf'
srv  llama_server: exiting due to model loading error

invalid vector subscript 是 MSVC 对 std::vector::at() 越界的报错措辞;其他 STL 实现可能抛 std::out_of_range 或表现为未定义行为。

原因分析

问题位于 src/llama-model.cpp 的 llama_model::load_tensors。按空闲显存切分设备的逻辑先读取设备内存:

ggml_backend_dev_memory(dev, &free, &total);

// devices can return 0 bytes for free and total memory if they do not
// have any to report. in this case, we will use the host memory as a fallback
if (free == 0 && total == 0) {
    ggml_backend_dev_memory(cpu_dev, &free, &total);
}
splits[i] = free;

随后求和并归一化:

float split_sum = 0.0f;
for (size_t i = 0; i < n_devices(); ++i) {
    split_sum += splits[i];
    splits[i] = split_sum;
}
for (size_t i = 0; i < n_devices(); ++i) {
    splits[i] /= split_sum;
}

再用 upper_bound 和 devices.at() 分配层:

const int layer_gpu = std::upper_bound(splits.begin(), splits.begin() + n_devices(),
                                       float(il - i_gpu_start)/act_gpu_layers) - splits.begin();
auto * dev = devices.at(layer_gpu).dev;

关键点是 free == 0 && total == 0 这个兜底条件只覆盖“free 和 total 都读不到”的情况。当目标模型已经占满显存时,GPU 报告的是 total != 0 而 free == 0,正好绕过保护,于是:

  1. splits[0] = 0
  2. split_sum = 0
  3. splits[0] = 0.0f / 0.0f → NaN
  4. 任何与 NaN 的比较都为假,std::upper_bound 返回 end()
  5. layer_gpu == n_devices()
  6. devices.at(n_devices()) → 越界

正常情况下 splits 归一化后最后一项恰为 1.0f,对小于 1.0f 的查找值 upper_bound 不可能返回 end();NaN 破坏了这个不变量。Issue 作者用 -lv 10 统计 draft 加载阶段的层分配日志也印证了这一点:-ngl 52 有 66 行分配日志并正常监听,-ngl 56 为 0 行并崩溃,说明在第一次 get_layer_buft_list 调用、日志打印之前就已死亡。

另有评论指出该问题并非 CUDA 专属:在 AMD Radeon 780M(HIP/ROCm 路径)上同样复现,与根因在后端无关的 src/llama-model.cpp 相符。Vulkan 后端不触发的原因可能是 ggml-vulkan.cpp 对集成 GPU 会汇总所有内存堆,报告的空闲量不会归零。

环境排查

  • 确认 llama.cpp 版本与构建方式:Issue 中复现的版本为 0.1.2-dev (build 10542, commit 521a64cd0),另在 build 10454(4df29be4f)复现,评论中使用 b10507(win-rocm-7.14-x64)、b10734(d5d993a09)官方 release 与自编译构建。
  • 确认操作系统:Windows(Windows 11 26200)。
  • 确认 CPU/编译器:Intel Core i5-13600KF;MSVC 19.51.36256.0(正文),评论中自编译使用 MSVC 19.44 / VS 2022 BuildTools。
  • 确认 GPU 与显存:NVIDIA GeForce RTX 4070 Ti 12 GB(驱动 610.88,CUDA 13.2)、RTX 5060 Ti 16 GB(sm_120);AMD Radeon 780M(gfx1103,UMA,6350 MiB 上报 / 空闲约 6130 MiB)。
  • 确认后端类型:CUDA、HIP/ROCm(PyTorch ROCm SDK,torch 2.12.0+rocm7.14.0)。
  • 确认是否单卡:n_devices() == 1。
  • 确认 draft 加载时机:目标模型是否已把显存占满、--n-gpu-layers(target)与 --n-gpu-layers-draft(-ngld)取值组合。
  • 确认投机解码类型:DFlash(--spec-type draft-dflash)会加载第二个模型;MTP(--spec-type draft-mtp)复用目标模型文件,不触发该问题。

解决步骤

  1. 优先尝试工作区绕过方案:在原本崩溃的命令上追加 --tensor-split 1(或 -ts 1)。设置后 llama-model.cpp 中 all_zero 为假,跳过按空闲显存切分的整段逻辑,splits[] 不会全零,0/0 → NaN 不会发生。Issue 评论已在 AMD 780M 上验证原本必定崩溃的完整命令加 -ts 1 后可正常加载并运行投机解码。
  2. 不要依赖降低目标模型 -ngl:评论中明确记录该做法不可靠,-ngl 40 -ngld 99 与 -ngl 20 -ngld 99 均崩溃且报错字节级一致;只有 -ngl 0 -ngld 0(不使用 GPU 卸载)能正常加载运行。
  3. 若使用集成 GPU(UMA)或 Vulkan 构建:可对比测试 Vulkan 后端是否可正常加载 draft,用于确认是否命中所描述的“空闲显存读 0”路径;Issue 中同一配置在 Vulkan 构建下可正常加载。
  4. 如具备自编译条件,可采用根因修复(可优先尝试):在 src/llama-model.cpp 中把兜底条件从 free == 0 && total == 0 放宽为 free == 0,并对归一化与 layer_gpu 做保护。评论中的验证方式是同时加入 split_sum == 0 保护与 layer_gpu clamp 后从源码构建(-DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=120),DFlash2 draft 每次都能加载运行。Issue 中提及的 PR #27657 / #27647 / #27887 尚未确认全部合并。
  5. 使用 MTP 投机解码作为临时替代(可优先尝试):评论指出 --spec-type draft-mtp 不加载第二个模型,draft 加载时内存查询仍返回 > 0,因此不会崩溃;这可以用于规避,但不适用于必须使用 DFlash draft 的场景。

验证方法

重新执行此前必定崩溃的 draft 加载命令,确认输出中不再出现 error loading model: invalid vector subscript,且 draft 模型成功加载、服务正常监听(例如 srv: listening on ...)或 llama-cli 正常进入推理。若沿用 Issue 中的调试思路,可加 -lv 10 观察 load tensors: layer N assigned to device ... 日志:修复或绕过生效后,draft 加载阶段应能看到非零数量的层分配日志,而不是 0 行即崩溃。

参考来源

ggml-org/llama.cpp #27454

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 29122

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注