使用 mergekit 合并大语言模型

社区文章 发布于 2024 年 1 月 9 日

image/jpeg

模型合并是一种将 两个或多个大语言模型(LLM) 组合成一个单一模型的技术。这是一种相对较新且实验性的方法,可以低成本地创建新模型(无需 GPU)。模型合并的效果出奇地好,并在 Open LLM Leaderboard 上产生了许多最先进的模型。

在本教程中,我们将使用 mergekit 库来实现它。具体来说,我们将回顾四种合并方法,并提供配置示例。然后,我们将使用 mergekit 创建我们自己的模型 Marcoro14-7B-slerp,它成为了 Open LLM Leaderboard (02/01/24) 上性能最好的模型。

代码可在 GitHubGoogle Colab 上获取。我推荐使用我的自动化笔记本来轻松运行 mergekit:🥱 LazyMergekit

特别感谢 mergekit 库的作者 Charles Goddard 审阅了本文。

注意:GML-Mistral-merged-v1 被错误地归类为 7B 参数模型(而不是 8.99B)。

🤝 合并算法

在本节中,我们将重点介绍目前在 mergekit 中实现的四种方法。请注意,还有其他方法,例如线性(linear)任务算术(Task Arithmetic)。如果您对关于模型合并的论文感兴趣,我推荐 Hugging Face 上的这个优秀合集

1. SLERP

球面线性插值 (Spherical Linear Interpolation, SLERP) 是一种用于在两个向量之间平滑插值的方法。它保持恒定的变化率,并保留向量所在球面空间的几何特性。

与传统的线性插值相比,有几个理由更倾向于使用 SLERP。例如,在高维空间中,线性插值可能导致插值向量的模长减小(即,它会减小权重的尺度)。此外,权重方向的变化通常比模长的变化代表更有意义的信息(如特征学习和表示)。

SLERP 的实现遵循以下步骤

  1. 将输入向量归一化为单位长度,确保它们代表方向而非大小
  2. 使用它们的点积计算这些向量之间的夹角。
  3. 如果向量几乎共线,为提高效率,它会默认使用线性插值。否则,SLERP 会根据插值因子 tt=0 = 100% 的第一个向量,t=1 = 100% 的第二个模型)和向量之间的夹角来计算缩放因子。
  4. 这些因子用于对原始向量进行加权,然后将它们相加以获得插值向量。

SLERP 目前是最流行的合并方法,但它一次只能合并两个模型。不过,仍然可以像 Mistral-7B-Merge-14-v0.1 中那样,分层合并多个模型。

配置示例

slices:
  - sources:
      - model: OpenPipe/mistral-ft-optimized-1218
        layer_range: [0, 32]
      - model: mlabonne/NeuralHermes-2.5-Mistral-7B
        layer_range: [0, 32]
merge_method: slerp
base_model: OpenPipe/mistral-ft-optimized-1218
parameters:
  t:
    - filter: self_attn
      value: [0, 0.5, 0.3, 0.7, 1]
    - filter: mlp
      value: [1, 0.5, 0.7, 0.3, 0]
    - value: 0.5
dtype: bfloat16

这是一个经典的 SLERP 配置,应用于两个模型的每一层。请注意,我们为插值因子 t 输入了一个梯度值。自注意力层和 MLP 层的参数将使用 OpenPipe/mistral-ft-optimized-1218mlabonne/NeuralHermes-2.5-Mistral-7B 的不同组合。其他层是这两个模型的 50/50 混合。

你可以在 Hugging Face Hub 上找到最终模型:mlabonne/NeuralPipe-7B-slerp

2. TIES

由 Yadav 等人在这篇论文中介绍的 TIES-Merging 旨在高效地将多个特定任务的模型合并成一个单一的多任务模型。它解决了模型合并中的两个主要挑战:

  • 模型参数的冗余:它识别并消除特定任务模型中的冗余参数。这是通过关注微调期间所做的更改,识别出前 k% 最显著的更改,并丢弃其余部分来实现的。
  • 参数符号之间的分歧:当不同模型对同一参数提出相反的调整时,会产生冲突。TIES-Merging 通过创建一个统一的符号向量来解决这些冲突,该向量代表了所有模型中最主要的变化方向。

TIES-Merging 分为以下三个步骤:

  1. 修剪(Trim):通过仅保留一小部分最显著的参数(密度参数)并将其他参数重置为零,来减少特定任务模型中的冗余。
  2. 选举符号(Elect Sign):通过基于累积幅度上最主要的方向(正或负)创建一个统一的符号向量,来解决不同模型之间的符号冲突。
  3. 不相交合并(Disjoint Merge):对与统一符号向量一致的参数值进行平均,不包括零值。

与 SLERP 不同,TIES 可以一次合并多个模型。

配置示例

models:
  - model: mistralai/Mistral-7B-v0.1
    # no parameters necessary for base model
  - model: OpenPipe/mistral-ft-optimized-1218
    parameters:
      density: 0.5
      weight: 0.5
  - model: mlabonne/NeuralHermes-2.5-Mistral-7B
    parameters:
      density: 0.5
      weight: 0.3
merge_method: ties
base_model: mistralai/Mistral-7B-v0.1
parameters:
  normalize: true
dtype: float16

通过这个配置,我们使用 Mistral-7B 作为基础模型来计算权重增量。我们合并了相同的两个模型:mistral-ft-optimized-1218 (50%) 和 NeuralHermes-2.5-Mistral-7B (30%),并进行了归一化。在这里,密度(density)意味着我们只保留每个模型 50% 的参数(另一半来自基础模型)。

请注意,配置中权重的总和不等于 1,但 `normalize: true` 参数会自动在内部对它们进行归一化。这个配置的灵感来自于 OpenHermes-2.5-neural-chat-7b-v3-1-7B 作者提供的参数。

你可以在 Hugging Face Hub 上找到最终模型:mlabonne/NeuralPipe-7B-ties

3. DARE

由 Yu 等人 (2023) 提出的 DARE 使用了与 TIES 类似的方法,但有两个主要区别:

  • 剪枝 (Pruning):DARE 将微调后的权重随机重置为其原始值(即基础模型的值)。
  • 重缩放 (Rescaling):DARE 对权重进行重缩放,以使模型输出的期望值大致保持不变。它将两个(或更多)模型的重缩放后的权重,按一定的缩放因子,加到基础模型的权重上。

Mergekit 对该方法的实现有两种方式:一种是带有 TIES 的符号选举步骤(`dare_ties`),另一种是不带(`dare_linear`)。

配置示例

models:
  - model: mistralai/Mistral-7B-v0.1
    # No parameters necessary for base model
  - model: samir-fama/SamirGPT-v1
    parameters:
      density: 0.53
      weight: 0.4
  - model: abacusai/Slerp-CM-mist-dpo
    parameters:
      density: 0.53
      weight: 0.3
  - model: EmbeddedLLM/Mistral-7B-Merge-14-v0.2
    parameters:
      density: 0.53
      weight: 0.3
merge_method: dare_ties
base_model: mistralai/Mistral-7B-v0.1
parameters:
  int8_mask: true
dtype: bfloat16

在这个配置中,我们使用 `dare_ties` 方法合并了三个基于 Mistral-7B 的不同模型。这次,我选择的权重总和为 1(总和应在 0.9 到 1.1 之间)。密度参数比论文中推荐的(<0.5)要高一些,但看起来它能持续给出更好的结果(参见此讨论)。

你可以在 Hugging Face Hub 上找到它:mlabonne/Daredevil-7B。它也是本文中最好的合并模型,甚至优于 Marcoro14-7B-slerp。

4. Passthrough (直通)

Passthrough 方法与之前的方法有显著不同。通过拼接不同大语言模型(LLM)的层,它可以产生具有奇异参数数量的模型(例如,用两个 7B 参数模型合成一个 9B 模型)。这些模型通常被社区称为“弗兰肯合并”或“弗兰肯斯坦模型”。

这项技术非常实验性,但它成功地创建了一些令人印象深刻的模型,比如使用两个 Llama 2 70B 模型创建的 goliath-120b。最近发布的 SOLAR-10.7B-v1.0 也使用了同样的想法,在他们的论文中称之为深度-向上扩展 (depth-up scaling)。

配置示例

slices:
  - sources:
    - model: OpenPipe/mistral-ft-optimized-1218
      layer_range: [0, 32]
  - sources:
    - model: mlabonne/NeuralHermes-2.5-Mistral-7B
      layer_range: [24, 32]
merge_method: passthrough
dtype: bfloat16

最终的弗兰肯合并模型将包含第一个模型的全部 32 层,以及第二个模型的额外 8 层。这样就创建了一个总共有 40 层和 8.99B 参数的弗兰肯合并模型。这个配置的灵感来自于 GML-Mistral-merged-v1

你可以在 Hugging Face Hub 上找到最终模型:mlabonne/NeuralPipe-9B-merged

💻 合并你自己的模型

在这一节中,我们将使用 mergekit 加载一个合并配置,运行它,并将生成的模型上传到 Hugging Face Hub。

首先,我们直接从源代码安装 mergekit,如下所示

!git clone https://github.com/cg123/mergekit.git
!cd mergekit && pip install -q -e .

在下面的代码块中,我们以 YAML 格式加载合并配置。我们还指定了合并后模型的名称以备后用。你可以将上一节的任何配置复制/粘贴到这里。

这次,我们将使用两个不同的模型:Marcoroni-7B-v3Mistral-7B-Merge-14-v0.1,并使用 SLERP 方法将它们合并。我们将配置保存为 yaml 文件,用作合并命令的输入。

import yaml

MODEL_NAME = "Marcoro14-7B-slerp"
yaml_config = """
slices:
  - sources:
      - model: AIDC-ai-business/Marcoroni-7B-v3
        layer_range: [0, 32]
      - model: EmbeddedLLM/Mistral-7B-Merge-14-v0.1
        layer_range: [0, 32]
merge_method: slerp
base_model: AIDC-ai-business/Marcoroni-7B-v3
parameters:
  t:
    - filter: self_attn
      value: [0, 0.5, 0.3, 0.7, 1]
    - filter: mlp
      value: [1, 0.5, 0.7, 0.3, 0]
    - value: 0.5
dtype: bfloat16
"""

# Save config as yaml file
with open('config.yaml', 'w', encoding="utf-8") as f:
    f.write(yaml_config)

我们使用以下参数运行合并命令:

  • --copy-tokenizer 从基础模型复制分词器 (tokenizer)
  • --allow-crimes--out-shard-size 将模型分块成更小的分片,以便在低内存的 CPU 上计算
  • --lazy-unpickle 启用实验性的惰性 unpickle 以降低内存使用

此外,某些模型可能需要 --trust_remote_code 标志(对于 Mistral-7B 来说不是这种情况)。

此命令将下载合并配置中列出的所有模型的权重,并运行所选的合并方法(大约需要 10 分钟)。

# Merge models
!mergekit-yaml config.yaml merge --copy-tokenizer --allow-crimes --out-shard-size 1B --lazy-unpickle

模型现在已经合并并保存在 `merge` 目录中。在上传之前,我们可以创建一个包含所有可复现性信息的 README 文件。下面的代码块定义了一个 Jinja 模板,并自动用合并配置中的数据填充它。

!pip install -qU huggingface_hub

from huggingface_hub import ModelCard, ModelCardData
from jinja2 import Template

username = "mlabonne"

template_text = """
---
license: apache-2.0
tags:
- merge
- mergekit
- lazymergekit
{%- for model in models %}
- {{ model }}
{%- endfor %}
---

# {{ model_name }}

{{ model_name }} is a merge of the following models using [mergekit](https://github.com/cg123/mergekit):

{%- for model in models %}
* [{{ model }}](https://huggingface.co/{{ model }})
{%- endfor %}

## 🧩 Configuration

\```yaml
{{- yaml_config -}}
\```
"""

# Create a Jinja template object
jinja_template = Template(template_text.strip())

# Get list of models from config
data = yaml.safe_load(yaml_config)
if "models" in data:
    models = [data["models"][i]["model"] for i in range(len(data["models"])) if "parameters" in data["models"][i]]
elif "parameters" in data:
    models = [data["slices"][0]["sources"][i]["model"] for i in range(len(data["slices"][0]["sources"]))]
elif "slices" in data:
    models = [data["slices"][i]["sources"][0]["model"] for i in range(len(data["slices"]))]
else:
    raise Exception("No models or slices found in yaml config")

# Fill the template
content = jinja_template.render(
    model_name=MODEL_NAME,
    models=models,
    yaml_config=yaml_config,
    username=username,
)

# Save the model card
card = ModelCard(content)
card.save('merge/README.md')

现在我们有了模型卡片,我们可以将整个文件夹推送到 Hub。

from google.colab import userdata
from huggingface_hub import HfApi

username = "mlabonne"

# Defined in the secrets tab in Google Colab
api = HfApi(token=userdata.get("HF_TOKEN"))

api.create_repo(
    repo_id=f"{username}/{MODEL_NAME}",
    repo_type="model"
)
api.upload_folder(
    repo_id=f"{username}/{MODEL_NAME}",
    folder_path="merge",
)

模型现在可以在 Hugging Face Hub 上找到:mlabonne/Marcoro14-7B-slerp。在另一个笔记本中,我们可以使用以下代码在免费的 T4 GPU 上尝试该模型:

!pip install -qU transformers accelerate

from transformers import AutoTokenizer
import transformers
import torch

model = "mlabonne/Marcoro14-7B-slerp"
messages = [{"role": "user", "content": "What is a large language model?"}]

tokenizer = AutoTokenizer.from_pretrained(model)
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
pipeline = transformers.pipeline(
    "text-generation",
    model=model,
    torch_dtype=torch.float16,
    device_map="auto",
)

outputs = pipeline(prompt, max_new_tokens=256, do_sample=True, temperature=0.7, top_k=50, top_p=0.95)
print(outputs[0]["generated_text"])

我们问了这个问题“什么是大语言模型?”,收到了以下输出:

大语言模型是一种人工智能(AI)系统,它在大量的文本数据上进行了训练。它旨在理解和生成类似人类的语言,预测句子或文档中接下来可能出现的单词或短语。这些模型使用复杂的算法和神经网络架构,从数据中学习并随着时间的推移提高其性能。一些著名的大语言模型包括 OpenAI 的 GPT-3 和 Google 的 BERT。

看起来不错,但我们需要更全面的评估。对于这种通用模型,有几个有趣的基准测试:

  • Chatbot Arena,根据人类投票编制了一个基于 Elo 的 LLM 排行榜。
  • MT-bench(链接同上),使用 GPT-4 作为评判者,对模型在一组多轮对话问题上的回答进行评分。
  • NousResearch 基准测试套件,它聚合了四个基准:AGIEval、GPT4ALL、TruthfulQA 和 Bigbench。GPT4ALL 本身包括 HellaSwag、OpenBookQA、Winogrande、ARC-Easy、ARC-Challenge、BoolQ 和 PIQA。
  • Open LLM Leaderboard,它聚合了六个基准:ARC、HellaSwag、MMLU、Winogrande、GSM8K 和 TruthfulQA。

不幸的是,我们无法将我们的模型提交到 Chatbot Arena。因此,我选择使用 Open LLM Leaderboard 和 NousResearch 基准来评估它。

我将我们的模型提交到了 Open LLM Leaderboard(“🚀 Submit here!” 标签页)。如引言中所示,它在排行榜上排名为最佳的 7B 参数模型。以下是排行榜上的完整结果:

image/png

Open LLM Leaderboard 的问题在于这些基准是公开的。这意味着人们可以在测试数据上训练 LLM 以获得更好的结果。通过合并最好的模型,我们也在污染我们自己的结果。可以安全地假设 Marcoro14-7B-slerp 被污染了,并且这次合并中使用的一些模型已经在测试集上进行了训练。如果你想创建最好的模型而不是“黑掉”排行榜,我建议只使用非合并模型来创建你自己的合并模型。

这就是为什么我们不想只依赖 OpenLLM Leaderboard。对于 NousResearch 基准测试套件,我使用了 🧐 LLM AutoEval,通过一个简单的 Colab 笔记本自动计算分数。以下是与优秀的 OpenHermes-2.5-Mistral-7B 相比的结果:

image/png

我们在每个基准上都比该模型取得了显著的提升。请注意,NousResearch 基准测试套件与 Open LLM Leaderboard 共享一些任务:ARC-Challenge、TruthfulQA、HellaSwag 和 Winogrande。据我所知,Bigbench 是唯一一个 100% 不同的基准(如果不是这样,请随时与我联系)。然而,我们在此次合并中使用的一个模型仍然可能在 Bigbench 上训练过。

结论

在本文中,我们介绍了使用四种不同方法合并大语言模型(LLM)的概念。我们详细介绍了 SLERP、TIES、DARE 和 passthrough 的工作原理,并提供了配置示例。最后,我们使用 mergekit 运行 SLERP 创建了 Marcoro14-7B-slerp 并将其上传到 Hugging Face Hub。我们在两个基准测试套件上获得了出色的性能:Open LLM Leaderboard(性能最佳的 7B 模型)和 NousResearch。如果你想创建自己的合并模型,我推荐使用我的自动化笔记本 🥱 LazyMergekit

另一种组合多个模型的方法是将它们合并成一个专家混合(MoE)架构。在下一篇文章中,我们将详细讨论如何做到这一点,并创建我们自己的 Mixtral 式模型。如果你喜欢这篇文章,请在 Hugging Face 和 Twitter @maximelabonne 上关注我。

社区

很棒的文章。在我看来,只有具有相同架构(例如,相同的层数、隐藏维度、注意力头)的模型才能用这种方法合并。这正确吗?你怎么知道哪些模型具有相同的架构?

·
文章作者

谢谢 @akratz ,是的,你说得对。通常,这会在模型卡片或配置文件中说明。

解释得很棒 ;)
我正在考虑使用 Slerp 或 Passthrough 来创建一个大模型的小型版本,并将其用作推测性草稿模型。

使用 Passthrough 时,均匀地选择层以避免层间距离过大是否有意义?(如 Solar 论文中提到的)例如:原始层从 1 到 10,然后选择 1, 4, 7, 10 来创建一个缩小一半的模型。

您会推荐哪种方法?
先谢谢了!

文章作者

谢谢!是的,通常你还希望更多地关注模型的首层和末层,因为它们更重要。

是的,但是如何进行模型汤(model soup)的基因合并演化呢?

很棒的文章,谢谢。TNG 发布了 DeepSeek-R1T-Chimera 模型 [1] - 还有其他人用非常大的模型做过这个吗?

[1] https://huggingface.co/tngtech/DeepSeek-R1T-Chimera

求求求求你,能出一个关于基因合并的笔记本吗?...我已经试了又试!

另外,我做了 dares-tie 合并,为什么它们总是效果不好?
一个简单的 ties 合并通常效果不错?

注册登录 以发表评论