[WIP Integration] LocateAnything-3B (MoonViT-SO-400M + Eagle MLP + Qwen2.5-3B): spatial localization failure and token count mismatch in cli

用户在使用 llama.cpp 的 llama-mtmd-cli 工具,将 nvidia/LocateAnything-3B 模型(架构:MoonViT-SO-400M + Eagle MLP 投影 + Qwen2.5-3B Instruct)集成到 mtmd / clip.cpp 框架时触发。模型

[WIP Integration] LocateAnything-3B (MoonViT-SO-400M + Eagle MLP + Qwen2.5-3B): spatial localization failure and token count mismatch in cli

[WIP Integration] LocateAnything-3B (MoonViT-SO-400M + Eagle MLP + Qwen2.5-3B): spatial localization failure and token count mismatch in cli

快速结论:该报错发生于使用 llama-mtmd-cli 运行 LocateAnything-3B 模型时,表现为空间定位完全失效(始终输出 <box><0><0><1000><1000></box>)且视觉 token 数不匹配(252 产生 vs 256 预期)。优先排查 Vision Encoder 层中的三个独立 bug:patch-embed conv 输出布局、2D RoPE 使用错误原语、位置嵌入扁平切片而非插值。

问题场景

用户在使用 llama.cppllama-mtmd-cli 工具,将 nvidia/LocateAnything-3B 模型(架构:MoonViT-SO-400M + Eagle MLP 投影 + Qwen2.5-3B Instruct)集成到 mtmd / clip.cpp 框架时触发。模型可以加载、预热并完成推理,但空间定位完全损坏,输出 token 数从预期的 256 减少到 252。

报错原文

Actual output:
  <ref>person</ref><box><0><0><1000><1000></box>

Expected output:
  <ref>person</ref><box><120><45><480><790></box>  (tight box around person)

Bug 1 — Token count: 252 produced, 256 expected
  Expected: (448/14)² / (2×2) = 1024 / 4 = 256 visual tokens
  Actual log: decoding image batch 1/1, n_tokens_batch = 252

原因分析

经过逐层数值验证,空间定位失败完全源于 Vision Encoder(MoonViT)中的三个独立 bug,而非 pos_row/pos_col 填充问题(该部分实际正确)。这三个 bug 导致最终合并视觉 token 与 PyTorch 参考实现的余弦相似度仅为 ≈0.11,即基本解耦,造成了定位退化为整幅图像。

Bug 1 — Patch-embed Conv 输出布局(moonvit.cpp):
ggml_conv_2d 输出维度顺序为 [OW, OH, OC, N](grid_w, grid_h, hidden),而非预期的 [OC, OW, OH, N]。直接扁平化为 [hidden, n_patches] 会导致通道与空间位置交错。需要添加 ggml_permute 修正。

Bug 2 — 2D RoPE 使用错误原语(build_rope_2d):
build_rope_2d(pos_row, pos_col, …, false) 使用分段(sectioned)方式:维度 0–35 由行位置旋转,维度 36–71 由列位置旋转。但 MoonViT 的 apply_rope 使用交错(interleaved)方式:相邻配对交替轴跨所有 72 维(偶数配对→列频率,奇数配对→行频率)。频率集相同但维度→轴映射完全不同,build_rope_2d 在结构上无法复现。

Bug 3 — 位置嵌入扁平切片而非插值(moonvit.cpp):
参考实现 Learnable2DInterpPosEmb 对可学习 64×64×dim 网格进行双三次插值到实际 patch 网格。当前代码只取了 [hidden, 4096] 表的前 n_patches 行(扁平切片),导致余弦相似度仅 0.371。

环境排查

  • llama.cpp 提交版本:e4406fed5 (build 8540)
  • GPU:NVIDIA GeForce GTX 1050 Ti (4030 MiB, compute 6.1)
  • OS / Arch:Linux x86_64, GNU 11.4.0
  • LLM 权重格式:Q4_K_M (1.96 GiB)
  • mmproj 权重格式:BF16 GGUF
  • 视觉编码器参数:image_size=896, patch_size=14, n_merge=2, n_embd=1152, n_head=16, n_layer=27

解决步骤

  1. 修复 Bug 1(Conv 输出布局):moonvit.cpp 中,ggml_conv_2d 后添加 permute 以将输出转换为正确布局:
    // ggml_conv_2d output is [grid_w, grid_h, hidden, 1]
    patches = ggml_cont(ctx0, ggml_permute(ctx0, patches, 1, 2, 0, 3)); // -> [hidden, grid_w, grid_h, 1]
    patches = ggml_reshape_2d(ctx0, patches, hidden_size, n_patches);    // patch idx = gh*grid_w + gw

    该修复可将 conv 层的余弦相似度从 0.44 提升到 1.00000。

  2. 修复 Bug 2(2D RoPE 原语):不可使用 build_rope_2d。建议在 GPU 上使用宿主预计算的 cos/sin(即参考 freqs_cis 的实部/虚部)在相邻维度对上进行旋转,替代 build_rope_2d。可优先尝试此方案。
  3. 修复 Bug 3(位置嵌入插值):不再对 [hidden, 4096] 表进行扁平切片,改为对 64×64 网格进行双三次插值到实际的 (grid_h, grid_w) 网格后再添加位置嵌入。注意:对于 patch 数 >4096 的网格,当前代码会通过 else 分支静默零化位置嵌入。
  4. 修复 Token 数量不匹配(252 vs 256):这是预处理不匹配问题。参考实现将图像裁剪至 4096 patches 以内,然后将每侧填充到 patch_size * merge = 28 的倍数。需要调整 llama.cpp 的 resize/letterbox 逻辑以匹配该行为,确保落在正确的网格上。
  5. (可选)小问题修复:moonvit.cpp 中的 LayerNorm eps1e-6 改为 1e-5,以匹配 PyTorch 参考实现中的默认值。

验证方法

应用上述三个主要修复后,重新运行相同的 llama-mtmd-cli 命令。验证最终合并的视觉 token 与 PyTorch 参考实现的余弦相似度应达到 1.0000(每层最大绝对值误差 ~1e-4)。同时确认输出不再为 <box><0><0><1000><1000></box>,而是生成正确的边界框坐标。

参考来源

ggml-org/llama.cpp #24020

GamsGo AI

AI 工具推荐

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

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

了解 GamsGo AI

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

celebrityanime
celebrityanime
文章: 7597

发表回复

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