Ollama 的 llama.cpp 兼容层解析:让旧版 GGUF 在新版 llama-server 上无缝加载
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.cpp 与 tools/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.txt 在 FetchContent_MakeAvailable 之后对 llama 和 mtmd 目标执行 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()
这样补丁就保持了"纯调用点插入"的性质。三种构建场景下的行为如下:
- 常规 fetch 构建:通过 CMake
FetchContent的PATCH_COMMAND自动应用补丁。compat.cmake 导出OLLAMA_LLAMA_CPP_COMPAT_PATCH_COMMAND变量,其内容是一次cmake -P脚本调用,把PATCH_DIR指向llama/compat,由 apply-git-patches.cmake 执行。 - 源码覆盖(source override):如果 CMake 通过
FETCHCONTENT_SOURCE_DIR_LLAMA_CPP指向了本地 llama.cpp 源码,llama/server/CMakeLists.txt 会在 configure 阶段同样执行一次补丁命令,并检查返回码。 - 开发者本地迭代:如果设置了
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 中的实际插入点一一对应:
- 主模型构造函数 →
translate_metadata(hook 插入点)。在llama_model_loader构造时、arch 读取之后立即调用。它检查已解析的元数据,当某个 handler 识别出"存量发布模型格式"时,就地修改内存中的gguf_context与ggml_context。函数返回true时,调用方会关闭 mmap(this->use_mmap = false)。头文件注释解释了原因:部分 handler(如 glm-ocr 的 gate+up FFN concat)需要通过 load_op 变换张量数据,而 llama.cpp 默认 mmap 路径会把张量直接绑定到 mmap 文件区域,没有可写位置落地变换后的字节;禁用 mmap 后加载器会预先分配真实的后端缓冲区,load_op 的覆写就能落在可写内存里。 - 主模型张量索引 →
should_skip_tensor(两处插入点,见 weights_map 填充循环)。用于隐藏文本加载器不应认领的嵌入 projector、vision、audio、MTP 等张量——注意它是从索引中剔除张量,而不是修改gguf_context。 - 主模型张量读取 →
maybe_load_text_tensor与maybe_load_text_tensor_range(load_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 张量"的量级,与被取代的整张量读取持平。 mtmd/clip构造函数 →translate_clip_metadata(插入点)。把单体 GGUF(monolithic GGUF,即文本与视觉权重同文件)改写为 llama.cpp 期望的 mmproj 形态的 clip 视图,使 clip.cpp 其余逻辑无需改动。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 翻译 |
qwen35、qwen35moe |
修正 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 翻译 |
qwen3vl、qwen3vlmoe |
补全缺失的 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 白名单(gemma3、gemma4、qwen35、qwen35moe、qwen25vl、qwen3vl、qwen3vlmoe、mistral3、deepseekocr、glmocr、llama4、nemotron_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"):
- 在 llama-ollama-compat.cpp 中实现
handle_<arch>();视觉模型还需实现handle_<arch>_clip(); - 在
translate_metadata/translate_clip_metadata的 dispatch 段中各加一行分派,并注意与相邻 handler 的先后顺序(涉及arch_name改写的 handler 必须放在依赖新名字的逻辑之前); - 若是单体视觉模型,同步更新 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.h、ggml.h、ggml-backend.h)编写,但有少量操作因公开 API 缺少等价 mutator 而依赖实现细节。README 用一张"依赖 → 用途 → 若上游补齐后的替代方案"表做了显式记账:
| 依赖 | 用途 | 上游补齐后的替代 |
|---|---|---|
直接写 ggml_tensor::type / ne[] / nb[] |
创建后对张量做 reshape/retype,用于内存内翻译 | 增加公开的张量 shape/type mutator |
rename_tensor 中 const_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 原生约定收敛的一个窗口。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00