除了权重,GGUF 还包含什么?——以及仍缺少什么?
摘要
本文探讨了 llama.cpp 用于语言模型的 GGUF 文件格式,重点介绍了其单文件便利性以及嵌入的聊天模板和特殊令牌的作用。还比较了不同的 Jinja 实现,并讨论了该格式仍缺少哪些内容。
暂无内容
查看缓存全文
缓存时间: 2026/05/14 21:26
# GGUF 里除了权重还有什么——以及还缺少什么? - NobodyWho
来源:https://nobodywho.ooo/posts/whats-in-a-gguf/
GGUF 是 `llama.cpp`(<https://github.com/ggml-org/llama.cpp>)用于语言模型的文件格式。GGUF 真正*精彩*的地方在于它只是一个单独的文件。相比之下,看看 HuggingFace 上一个典型的 safetensors 仓库(<https://huggingface.co/Qwen/Qwen3.5-0.8B/tree/main>),那里散落着一堆必要的 JSON 文件;或者看看典型的 Ollama 模型(<https://ollama.com/library/qwen3.5:0.8b>),它是一个 OCI 格式,内部包含 layers json、go 模板等等。内容大致相同,但 GGUF 通过将所有这些东西*整合*到一个文件中,使得使用更加便捷。但是这些“东西”究竟是什么?它们是否涵盖了一切所需?
## 聊天模板
对话式语言模型的训练序列遵循特定格式,看起来有点像对话。例如,Gemma4 的格式如下:
```
<|turn>user Hi there! <|turn>model Hi there, how can I help you today?
```
……而 LFM2 的格式模板如下:
```
<|im_start|>user Hi there!<|im_end|>
<|im_start|>assistant Hi there, how can I help you today?<|im_end|>
```
……这只是一个基本示例。一旦我们开始添加更高级的功能,比如如何以及何时格式化推理块、如何呈现工具描述、工具调用及其响应,以及如何对多模态消息(图像、音频、视频等)进行编码,情况就变得复杂得多了。所有这些都由一个*聊天模板*处理,即一个 jinja2 模板语言编写的脚本。例如,Gemma 4 附带的聊天模板(<https://huggingface.co/google/gemma-4-E4B-it/raw/main/chat_template.jinja>)。默认聊天模板存储在 GGUF 元数据的 `tokenizer.chat_template` 键下。一个模型*可能*有多个聊天模板。例如,一个支持工具调用的模板,另一个不支持。大多数情况下,模型附带一个单一的、整体式的聊天模板,只有在指定了工具时才会处理工具调用相关的内容,但在某些模型中,你确实需要查找特定于工具的聊天模板。
Jinja2 毫无疑问是一种编程语言——它有循环、条件判断、赋值、列表、字典等。因此,任何对话式 LLM 应用都必须内置一个编程语言解释器,能够在每次添加新消息时运行类似 Gemma 附带的那段约 250 行的 jinja 脚本。HuggingFace transformers 使用 jinja2(经典的 Python 库),`llama.cpp` 的 `llama-server` 和 `llama-cli` 使用它们自己的 jinja 实现(<https://github.com/ggml-org/llama.cpp/tree/85d482e6b6706648070f620797e54f1a6a0ff3d8/common/jinja>)(不要与 `libllama` API 中暴露的、有些令人困惑的 `llama_chat_apply_template`(<https://github.com/ggml-org/llama.cpp/blob/85d482e6b6706648070f620797e54f1a6a0ff3d8/src/llama-chat.cpp#L240>)混淆,后者直接在 C++ 中硬编码了少数几种聊天格式——这是在真正的 jinja 实现出现之前的一个迷人遗迹),而 NobodyWho 使用 `minijinja`(<https://github.com/mitsuhiko/minijinja>),这是 jinja 的原作者用纯 Rust 重新实现的版本(不要与 `minja`(<https://github.com/google/minja>)混淆,后者是一个曾经被 `llama.cpp` 使用的最小化 jinja 库)。这些 jinja 实现之间存在*显著的性能差异*(<https://gitlab.com/AsbjornOlling/chat-template-benchmark>)。但在本地 LLM 应用中,聊天模板并不是性能瓶颈,所以不值得为此争论。
## 特殊标记
语言模型会为你提供的任何标记序列无休止地输出下一个标记,所以我们需要某种方式来阻止它们。典型的解决方案是某种序列结束标记。这个想法是,每当模型生成这样一个标记时,推理引擎就停止生成。这是特殊标记的一个例子。特殊标记通常具有比它们标记化的字母更广泛的语义含义。它们通常是那些不应该显示给用户的标记,尽管它们(通常)仍然有文本表示,所以*可以*显示。例如,Gemma4 的几个标记:
| 标记 ID | 文本表示 | 用途 |
|--------|------------|------|
| 1 | `` | 序列结束,模型生成此标记以停止生成。 |
| 2 | `` | 序列开始,预置在输入前面。 |
| 46 | `<|tool_call|>` | 标记工具调用的开始。 |
| 47 | `` | 标记工具调用的结束。 |
| 105 | `<|turn|>` | 对话回合的开始。 |
| 106 | `` | 对话回合的结束。 |
## 采样器配置
语言模型输出一个下一个标记概率的分布。从这个分布中选择一个标记称为采样。最简单的方法是从加权分布中随机选择。但我们可以做更多。研究表明,在选择一个具体标记之前,对概率分布应用一些变换可以获得更好的结果。当研究实验室发布一个新模型时,他们通常会包含一个特定的推荐采样器配置。我经常看到人们从某个 markdown 文件中复制粘贴这些值,以获得更好的模型响应。为了省去用户的这一步,我们开始将一小批精选模型上传到我们的 HuggingFace 页面(<https://huggingface.co/NobodyWho/models>),并以我们自己设计的格式捆绑了推荐的采样器设置。这很有效,但意味着每个模型都需要经过 NobodyWho 侧的转换才能使用。令人高兴的是,GGUF 格式最近的一个新增功能(<https://github.com/ggml-org/llama.cpp/pull/17120>)允许直接在模型文件中指定采样器链。这使我们自己的自定义格式变得过时——而这正是我们想要的结果。
## 采样器链顺序
我非常喜欢这个 Web 应用(<https://artefact2.github.io/llm-sampling/>),可以快速感受不同采样步骤的效果。如果你拖放各个步骤,你会发现采样步骤的顺序对最终的分布情况影响很大。让我感到沮丧的是,大多数采样器配置格式(包括 Ollama 镜像的 JSON 文件和 HuggingFace 的 `generation_config.json`)都没有任何指定采样步骤顺序的方式。我很高兴 GGUF 标准为此包含了 `general.sampling.sequence` 字段,它允许你指定顺序。但是,仍然有很多 GGUF 模型会省略这个字段,并期望采用“`llama.cpp` 默认设置的”隐式顺序。好吧,虽然隐式,但也能工作。
## 还缺少什么?
好的推理引擎旨在为不同的语言模型提供统一的接口。GGUF 元数据中的“额外内容”涵盖了很大一部分,因此解析和使用这些内容可以让我们避免大量特定于模型的代码路径。
### 仍然缺失:工具调用格式
似乎每个推理引擎都有硬编码的路径来解析不同的工具调用格式。例如,Qwen3 的工具调用看起来像这样:
```
{"name": "get_weather", "arguments": {"location": "Copenhagen"}}
```
Qwen3.5 的工具调用看起来像这样:
```
Copenhagen
```
……而 Gemma4 的工具调用看起来像这样:
```
<|tool_call>call:get_weather{city:<|"|>Copenhagen<|"|>}
```
目前,每当新模型发布时,大量不同的推理引擎都争相实现解析器。如果模型文件能包含一个语法(grammar),我们可以从中推导出解析器,那将是对 GGUF 标准的一个极好补充。
在 NobodyWho 中,我们在工具调用方面更进一步(有点独特?),因为我们为传入的特定工具生成一个独特的约束语法。这意味着我们可以保证工具调用的类型安全。这对于最小的模型(1B 或更小)*尤其*有用,这些模型有时会出错,例如在需要整数时传入了浮点数。虽然指定一个我们可以从中推导出通用工具调用解析器的语法会很有用,但 NobodyWho 仍然需要实现为每个传入的特定工具生成语法的功能。设计一种元语法格式,用来为特定工具推导具体语法,再从中推导出解析器——这是一个有趣的问题。
### 仍然缺失:思考标记
这绝对是最容易添加的内容。上游的 HuggingFace 仓库(<https://huggingface.co/google/gemma-4-E2B/blob/main/tokenizer_config.json#L31>)已经开始包含一个 `think_token` 字段。这对于将生成输出的思考部分与主体输出分开非常有用,因为思考部分通常应该被去除或使用不同的方式渲染。不知为何,下游的 GGUF 转换(<https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/blob/main/gemma-4-E2B-it-Q4_0.gguf>)通常不包含这个字段。这使得基于 GGUF 的推理引擎无法将思考流与主体输出分开,除非为特定模型系列编写特定的代码路径。将 `think_token` 添加到标准的 GGUF 转换流程中就能解决这个问题。我们应该这样做。
### 仍然缺失:投影模型
多模态 LLM 交互(即让 LLM 原生看到图像和音频,而不仅仅是文本)需要一个额外的模型来处理非文本输入,称为“投影模型”。惯例是传入*两个* GGUF 文件:一个用于主语言模型,另一个较小的模型用于处理图像和音频。这打破了单个文件的便捷性。如果单个 GGUF 文件能将投影模型的权重和配置捆绑在主文件中,那将是一个巨大的改进。投影模型通常约 1GB 大小——这个开销足够大,我们在不使用它时肯定希望跳过。但我认为提供两个 GGUF 变体是合理的:一个包含投影权重,另一个不包含。这样我们就可以恢复到只管理一个下载 URL、一个磁盘缓存文件等的状态。
### 仍然缺失:支持功能列表
模型并不都支持相同的功能,并且从 GGUF 文件中不容易检测到实际支持哪些功能。一些模型支持图像输入,另一些不支持。目前处理这个问题的最佳方法是,当传入投影模型时假定支持图像。一些模型原生支持工具调用,另一些不支持。目前处理这个问题的最佳方法是,对聊天模板进行子串匹配,看它是否试图渲染工具 JSON 模式的列表。这显然很 hacky。一些模型会生成思考块,另一些不会。由于思考标签通常不在 GGUF 元数据中,我不确定是否有好方法来判断我们是否期望从模型中生成思考块。我非常希望 GGUF 社区能够开始在模型文件中添加功能标志,这样像我们这样的模型无关推理库就可以在消费程序尝试执行工具调用(而模型本身并不原生支持)时,更一致地提供错误消息和警告。
## 结论
我爱 GGUF。我爱它,因为它只是一个单独的文件,涵盖了*正确*运行一个模型所需的所有“东西”,而无需添加大量特定于模型的代码路径。我也爱 GGUF,因为它是一种开放、可扩展的格式,拥有强大的社区支持。这意味着我们可以共同努力加强标准,在能够轻松地在应用程序中更换模型的同时,保持出色的开发者体验,而无需重写任何代码。
这篇文章涵盖了许多 GGUF 元数据已经很棒的方面,以及我们希望改进的一些方面。如果你想关注我们在这个领域的工作,请在接下来几周内关注我们的 HuggingFace 页面和 `llama.cpp` 的 issue 板。
*本文完全由人类撰写。没有一个词是由机器编造的。*
相似文章
llama.cpp 发生了什么
llama.cpp 的一次重大更新要求重新生成所有之前生成的 GGUF 文件,这表明模型格式发生了重大的不兼容变化。
PSA: unsloth/GLM-5.2-GGUF 正在上传
unsloth 已将 GLM-5.2 的 GGUF 版本上传至 Hugging Face,为 llama.cpp、vLLM 和 SGLang 等多种推理引擎提供了可直接使用的模型文件。
huihui-ai/Huihui-GLM-5.2-abliterated-GGUF
Hugging Face 上发布了已消除限制的 GLM-5.2 模型的量化 GGUF 版本,可使用 Transformers、llama.cpp 和 vLLM 等工具进行本地推理。
@WaleedAhmad1a10: 查看 Qwen 3.5 27B MoQ 的 GGUF 文件:
Hugging Face 仓库 (kaitchup/Qwen3.6-27B-GGUF-MoQ) 提供了 Qwen3.6-27B MoQ 模型的 GGUF 量化权重,支持使用 llama.cpp 和 Ollama 等工具进行本地推理。
unsloth/inkling-GGUF
unsloth/inkling-GGUF 页面提供了 Inkling 的量化 GGUF 版本。Inkling 是 Thinking Machines 开发的一个 975B 参数的多模态 MoE 模型(41B 活跃),设计用于文本、图像和音频输入,拥有开放权重,并支持通过 Unsloth、SGLang 和 vLLM 等库进行本地部署。