首页
/ Ollama 的 llama.cpp 兼容层解析:让旧版 GGUF 在新版 llama-server 上无缝加载

Ollama 的 llama.cpp 兼容层解析:让旧版 GGUF 在新版 llama-server 上无缝加载

2026-09-04 09:21:10作者:吴年前Myrtle

Ollama 正在把推理后端从自研 C++ 加载器迁移到上游 llama-server,但大量已发布的 GGUF 模型在元数据(arch 名、KV 键)和张量布局上与 llama.cpp 的直接预期不一致。llama/compat/ 目录中维护的正是为此设计的进程内兼容层:它不修改磁盘上的模型文件,而是在加载时于内存中完成翻译,使这些模型无需重新下载或重新创建即可运行。本文基于 llama/compat/README.md 展开,结合补丁、CMake 集成与源码入口,完整剖析该兼容层的钩子机制、支持矩阵与二次开发方式。

一、兼容层的定位与生命周期

根据 README 的表述,该目录保存的是一个临时性的 in-process 兼容层,服务对象是"现有已发布的 Ollama GGUF"——即其元数据或张量布局尚不匹配 llama.cpp 直接预期的文件。层的工作方式是在加载时对文件做内存级翻译,用户在过渡到 llama-server 期间无需 re-pull 或 re-create 模型。

其"目标终态"也很明确:已发布模型与新创建模型都直接使用 llama.cpp 兼容的元数据和张量布局落盘,届时整个 llama/compat/ 目录可以被删除。因此在阅读其设计时,应把它理解为补丁模型(patch model)而非长期架构——这正是 README 开篇反复强调 "short lived" 的原因。

目录结构上,该层由两类文件组成,职责不同:

文件 职责
llama-ollama-compat.h / llama-ollama-compat.cpp 兼容层入口点与各架构(per-architecture)处理器
llama-ollama-compat-util.h / llama-ollama-compat-util.cpp KV 编辑、张量重命名、skip 前缀跟踪、张量加载操作与小型张量重打包原语
001-llama-cpp-hooks.patch 对 llama.cpp 文件的少量追加式调用点修改,当前只触及 src/llama-model-loader.cpptools/mtmd/clip.cpp
002-llama-cpp-ui-empty-assets.patch 允许 llama.cpp UI 的 embed helper 在没有 UI 资产时生成空资产表
compat.cmake CMake 胶水,调用共享的幂等补丁应用器 cmake/apply-git-patches.cmake
models/ 平行的"新架构"层:实现 llama.cpp 尚不支持的架构,每个架构通过一个小注册补丁加入

README 特别区分了两者的语义差别:models/ 下的文件是为 llama.cpp 添加它还没有的架构,而上面这些文件是把已有 GGUF 翻译到 llama.cpp 已经具备的架构上

二、构建期集成:补丁如何在 CMake 中生效

兼容层的分发设计非常讲究"Ollama 代码留在 Ollama 树内"。compat.cmake 的注释明确写道:兼容源文件不会被拷贝进 FetchContent 拉取到的 llama.cpp 树中,而是由 llama/server/CMakeLists.txtFetchContent_MakeAvailable 之后对 llamamtmd 目标执行 target_sources() 直接链接:

# llama/server/CMakeLists.txt(节选逻辑)
file(GLOB _compat_sources CONFIGURE_DEPENDS ${OLLAMA_LLAMA_CPP_COMPAT_DIR}/*.cpp)
foreach(_compat_target IN ITEMS llama mtmd)
    if(TARGET ${_compat_target})
        target_sources(${_compat_target} PRIVATE ${_compat_sources})
        ...
    endif()
endforeach()

这样补丁就保持了"纯调用点插入"的性质。三种构建场景下的行为如下:

  1. 常规 fetch 构建:通过 CMake FetchContentPATCH_COMMAND 自动应用补丁。compat.cmake 导出 OLLAMA_LLAMA_CPP_COMPAT_PATCH_COMMAND 变量,其内容是一次 cmake -P 脚本调用,把 PATCH_DIR 指向 llama/compat,由 apply-git-patches.cmake 执行。
  2. 源码覆盖(source override):如果 CMake 通过 FETCHCONTENT_SOURCE_DIR_LLAMA_CPP 指向了本地 llama.cpp 源码,llama/server/CMakeLists.txt 会在 configure 阶段同样执行一次补丁命令,并检查返回码。
  3. 开发者本地迭代:如果设置了 OLLAMA_LLAMA_CPP_SOURCE,补丁被有意跳过,方便开发者在自己的 llama.cpp 树上手工迭代。

幂等性是这套机制的关键细节。apply-git-patches.cmake 会递归收集 PATCH_DIR 下所有 *.patch,按文件名数字前缀排序后依次应用;应用前先跑 git apply --reverse --check 探测补丁是否已应用过(能干净回退即视为已应用),从而保证重复 configure/rebuild 不会报 "already applied"。若应用失败,脚本会直接 FATAL_ERROR 并提示删除保留的源码目录或针对固定版本的源码重新生成补丁。

三、加载期钩子:五个介入点

兼容层在 llama.cpp 加载器的少量固定钩子点运行。llama-ollama-compat.h 中声明了完整的 API 面,与 001-llama-cpp-hooks.patch 中的实际插入点一一对应:

  1. 主模型构造函数 → translate_metadatahook 插入点)。在 llama_model_loader 构造时、arch 读取之后立即调用。它检查已解析的元数据,当某个 handler 识别出"存量发布模型格式"时,就地修改内存中的 gguf_contextggml_context。函数返回 true 时,调用方会关闭 mmapthis->use_mmap = false)。头文件注释解释了原因:部分 handler(如 glm-ocr 的 gate+up FFN concat)需要通过 load_op 变换张量数据,而 llama.cpp 默认 mmap 路径会把张量直接绑定到 mmap 文件区域,没有可写位置落地变换后的字节;禁用 mmap 后加载器会预先分配真实的后端缓冲区,load_op 的覆写就能落在可写内存里。
  2. 主模型张量索引 → should_skip_tensor(两处插入点,见 weights_map 填充循环)。用于隐藏文本加载器不应认领的嵌入 projector、vision、audio、MTP 等张量——注意它是从索引中剔除张量,而不是修改 gguf_context
  3. 主模型张量读取 → maybe_load_text_tensormaybe_load_text_tensor_rangeload_all_data 插入点load_data_range 插入点)。在 llama.cpp 正常读文件之前应用已注册的文本侧加载操作,例如 FFN concat 或 dtype 提升。README 提到一个与上游演进相关的细节:llama.cpp b10729 之后,整张量读取 load_data_for 被基于 slab 的 load_data_range 取代,llama-quantize 这类单张量读取工具走的是后者,因此 maybe_load_text_tensor_range 采用单槽缓存策略——每个 loader 只为一个活动张量物化操作的全部输出(下一个张量的第一个 range 到来时即驱逐),每次调用从该缓存中服务请求的 (offset, size) 范围。这样量化的内存占用保持在"一个 op 张量"的量级,与被取代的整张量读取持平。
  4. mtmd/clip 构造函数 → translate_clip_metadata插入点)。把单体 GGUF(monolithic GGUF,即文本与视觉权重同文件)改写为 llama.cpp 期望的 mmproj 形态的 clip 视图,使 clip.cpp 其余逻辑无需改动。
  5. mtmd/clip 张量加载循环 → maybe_load_tensor插入点)。应用 clip 侧的加载操作:F16→F32 提升、QKV 合并、张量重打包、张量切分或零填充。

补丁中还有一个小插入点:在 clip_n_mmproj_embd() 的上游 switch 之前调用 maybe_clip_mmproj_embd插入点),为那些 projector 元数据已遵循上游命名、但固定版本的 llama.cpp 缺少对应 projector type 的 Ollama 兼容场景返回 embedding 尺寸。

非 Ollama 文件的处理:所有入口点都是按 arch 做检测的,对任何非 Ollama 文件每个入口都是 no-op,不做任何改动。另外,设置环境变量 OLLAMA_LLAMA_CPP_COMPAT=0 可以关闭全部钩子主体——这在 llama-ollama-compat.cpp 中由 compat_disabled() 实现(读取环境变量并比较是否为 "0"),用途有二:内部 create 时刻的校验,以及针对"已知磁盘上已是 llama.cpp 兼容格式"的模型跳过无谓的钩子开销。server/quantization.go 中同样定义了 llamaCppCompatEnv 常量,说明该开关也被 Ollama 服务端的量化流程(调用 llama-quantize)所感知——量化后的输出文件若不含嵌入的兼容张量,服务端还会通过 restoreEmbeddedCompatibilityTensors 恢复它们,因为 llama.cpp 的文本模型加载器有意不认领这些张量。

四、支持的转换矩阵(Supported Transformations)

README 中的表格跟踪的是分发表面(dispatch surface),精确的 KV 与张量映射以 llama-ollama-compat.cpp 内 handler 的注释为准。完整矩阵如下:

内部 arch / 标记 文本侧处理 Clip/mmproj 侧处理
gemma3 规范化 Gemma 3 元数据、tokenizer 字段与嵌入的 vision/projector 张量 Gemma 3 projector 翻译
gemma3 + 嵌入标记(embeddinggemma 映射到 gemma-embedding 元数据并修正 embedding dense/norm 张量 n/a
bert + Snowflake 标记(snowflake-arctic-embed2 修正 Snowflake Arctic Embed 2 的 tokenizer 元数据 n/a
gemma3n 规范化 tokenizer/EOS 元数据、截断 vocab 形状张量、隐藏未使用的嵌入 vision/audio/projector 张量 n/a
gemma4 规范化 tokenizer 元数据,向文本加载器隐藏嵌入的 audio/vision/projector 张量 Gemma 4 视觉/音频 projector 翻译(面向 GGUF blob)
gptoss 映射到 gpt-oss、拷贝 KV、注入缺失的 expert FFN 元数据并重命名张量 n/a
lfm2 重命名 norm 张量并修正 feed-forward 元数据 n/a
olmo3 映射到 OLMo2 兼容的加载路径 n/a
mistral3 修正 RoPE/YaRN 元数据并隐藏嵌入的 vision/projector 张量 Pixtral 风格 projector 翻译
qwen35qwen35moe 修正 Qwen3.5/Qwen3-VL 风格文本元数据、翻译嵌入的 MTP 张量、隐藏嵌入的 vision/projector 张量 Qwen3-VL merger 风格 projector 翻译
qwen3next 规范化混合注意力 KV-head 元数据,并把 SSM dt 张量重命名为 llama.cpp 期望的名字 n/a
qwen25vl 映射到 qwen2vl 的元数据约定 Qwen2.5-VL projector 翻译
qwen3vlqwen3vlmoe 补全缺失的 Qwen3-VL 元数据并隐藏嵌入的 vision/projector 张量 Qwen3-VL projector 翻译,包括 QKV 合并与 patch-embedding 切分/重打包
deepseekocr 映射到 deepseek2-ocr、注入缺失的 OCR/MoE 元数据、隐藏嵌入的 SAM/vision/projector 张量 DeepSeek OCR projector 翻译
glmocr 将 GLM OCR 元数据/张量映射到 llama.cpp 兼容视图 GLM OCR projector 翻译
glm4moelite 将 GLM-4.7 Flash MLA 元数据映射到 deepseek2 路径并修正特殊 token 元数据 n/a
laguna 将旧版 attention-gate 张量与 SWA RoPE 元数据重命名为当前 llama.cpp 名称 n/a
nemotron_h_moe 修正 latent-FFN 变体并隐藏 MTP 张量 n/a
nemotron_h_omni 选择 Nemotron 文本加载器并向文本加载器隐藏 audio/vision/projector 张量 Nemotron V2 VL projector 翻译;音频仍保持禁用
llama(带 Llama 3 标记) 修正 Llama 3 的 tokenizer 元数据 n/a
llama4 向文本加载器隐藏嵌入的 vision/projector 张量 Llama 4 projector 翻译
clip projector 但无 clip.projector_type n/a 将 LLaVA/BakLLaVA projector 默认为 clip.projector_type=mlp

llama-ollama-compat.cpp 的 dispatch 段 可以看到,这些 handler 是按 arch 名串接的一连串 if,且存在明确的顺序语义:例如 embeddinggemma 必须在 gemma3 之前运行(因为它把 arch_name 切换为 gemma-embedding,后续检查与加载器的 KV 前缀依赖这个名字);qwen25vl 必须先于任何面向 qwen2vl 的 handler(它把 arch 切到 qwen2vl);glm4moelite 同理切换到 deepseek2。handler 内部先用 detect_ollama_*() 判定是否为 Ollama 格式文件,不匹配则直接返回——这保证了非 Ollama 文件完全不被触碰。

五、实际用法:同一文件同时作为 --model--mmproj

对单体视觉 GGUF(文本 + 视觉权重同在一个文件),README 给出的用法是:

llama-server --model /path/to/ollama-blob --mmproj /path/to/ollama-blob

把同一个单体 GGUF 同时传给 --model--mmproj 之所以可行,是因为两条加载器各自应用各自的翻译:主模型加载器走 translate_metadata/should_skip_tensor 隐藏视觉张量并只加载文本部分;clip 加载器走 translate_clip_metadata 拿到改写后的 mmproj 视图。

在 Ollama 服务端这一行为是自动的。llm/llama_server.go 中的 NewLlamaServerRunner 会检查 GGUF 是否内嵌 v.* 视觉张量,且架构位于 compatClipArches 白名单(gemma3gemma4qwen35qwen35moeqwen25vlqwen3vlqwen3vlmoemistral3deepseekocrglmocrllama4nemotron_h_omni),若用户没有显式传 projector,就把主模型路径自身塞进 projectors。白名单存在的防御性理由写在注释里:如果对一个尚无 clip handler 的架构自动开启 --mmproj,上游 clip 加载器会看到未翻译的 Ollama 张量并中止模型加载。因此新增单体视觉模型时必须同步更新这份白名单,使 C++ 侧与 Go 侧的 clip 覆盖保持一致。

六、如何新增一个架构的兼容支持

README 给出了明确的扩展步骤,与 llama-ollama-compat.h 的头部注释一致("每个 arch 的逻辑位于 .cpp 匿名命名空间的 handle_<arch>() 函数中;新增一个 arch = 一个新 handler + 每个 translate_* 入口点一行 dispatch"):

  1. llama-ollama-compat.cpp 中实现 handle_<arch>();视觉模型还需实现 handle_<arch>_clip()
  2. translate_metadata / translate_clip_metadata 的 dispatch 段中各加一行分派,并注意与相邻 handler 的先后顺序(涉及 arch_name 改写的 handler 必须放在依赖新名字的逻辑之前);
  3. 若是单体视觉模型,同步更新 llm/llama_server.go 中的 compatClipArches 白名单,让 Ollama 把主 GGUF 作为 --mmproj 传给 llama-server。

如果新增的架构是 llama.cpp 根本不认识的新架构(而非翻译到已有架构),则走 models/ 平行层:实现架构源文件,并以 models/003-llama-cpp-laguna-metal.patch 这类小注册补丁接入,由 apply-git-patches.cmake 的按数字前缀排序机制保证与应用顺序。

七、llama.cpp 版本升级后如何重新生成补丁

当 llama.cpp 升级(bump)导致插入点漂移时,README 给出的流程是:对一份新的 checkout 重新手工应用这些编辑,然后导出 diff 覆盖补丁文件:

cd /path/to/llama.cpp
git diff -- \
    src/llama-model-loader.cpp \
    tools/mtmd/clip.cpp \
    > /path/to/ollama/llama/compat/001-llama-cpp-hooks.patch

这与 apply-git-patches.cmake 的失败提示语("The pinned source is neither clean nor compatible with this patch... regenerate the patch against the pinned source before retrying")形成闭环:补丁与固定版本的 llama.cpp 源码强绑定,源树不干净或不兼容时宁可构建失败也不静默跳过。

八、实现层面的取舍:对非公开 API 的依赖

兼容代码主体针对公开 API(gguf.hggml.hggml-backend.h)编写,但有少量操作因公开 API 缺少等价 mutator 而依赖实现细节。README 用一张"依赖 → 用途 → 若上游补齐后的替代方案"表做了显式记账:

依赖 用途 上游补齐后的替代
直接写 ggml_tensor::type / ne[] / nb[] 创建后对张量做 reshape/retype,用于内存内翻译 增加公开的张量 shape/type mutator
rename_tensorconst_cast<char *>(gguf_get_tensor_name(...)) 原地重命名 gguf 张量 增加公开的 gguf_rename_tensor helper
src/llama-model-loader.h 前向声明 llama_model_loader 作为 per-loader 注册表的 opaque key,指针从不解引用 将注册表 key 换成 const void *

两个 helper 还值得单独说明:

  • reclaim_slot_as:当某个 clip handler 把一个源张量拆成多个目标张量时,把一个"孤儿"张量槽位重新用作合成张量。之所以需要它,是因为 clip 元数据加载恰好按源文件分配了足够数量的张量槽位,没有富余可放新张量。
  • load-op 注册表覆写忽略调用方提供的 file_offset:当某个张量存在已注册加载操作时,操作在翻译阶段(重命名改变张量名之前)就已捕获了自己的源偏移,因此以操作自持的偏移为准,而不是以调用方传入的偏移为准——这避免了"先重命名再按新名字查旧偏移"这类错位。

结语

llama/compat/ 是 Ollama 推理后端切换期的关键工程装置:它以"补丁只插调用点、源码留在本仓库"的 CMake 集成方式、五个精确的加载期钩子、按 arch 检测且对非 Ollama 文件零侵入的 handler 机制,让存量 GGUF 与新 llama.cpp 在过渡期内继续协同工作。对阅读者而言,理解它有三层价值:一是掌握"同一 GGUF 文件经双视图翻译"这一单体多模态加载方案的实现细节;二是把支持矩阵与 llm/llama_server.go 白名单、server/quantization.go 环境开关串起来,看到兼容层如何贯穿加载、量化与运行三条链路;三是掌握随 llama.cpp 版本 bump 重新生成补丁、以及为新增架构扩展 handler 的完整工作流。由于它被明确定义为短生命周期的过渡设施,跟踪该目录的收缩过程本身,也是观察 Ollama 模型格式向 llama.cpp 原生约定收敛的一个窗口。

登录后查看全文
热门项目推荐
相关项目推荐