Notebooks
H
Hugging Face
Fine Tuning Code Llm On Single Gpu

Fine Tuning Code Llm On Single Gpu

zh-CNhf-cookbooknotebooks

在单个 GPU 上针对自定义代码微调代码 LLM

作者: Maria Khalusova

公开发布的代码 LLM,如 Codex、StarCoder 和 Code Llama,在生成遵循通用编程原则和语法的代码方面表现出色,但它们可能不符合组织的内部惯例,或者不了解某些特定的库。

在这个 notebook 中,我们将展示如何微调代码 LLM 来更好的理解你们公司或组织的代码风格和习惯。由于代码 LLM 非常大,按照传统的微调方式可能会消耗大量资源。但不用担心!我们会教你一些技巧,让你只用单个 GPU 就能完成微调工作。

数据集

对于这个例子,我们选择了 GitHub 上 Hugging Face 的前 10 个公共仓库。我们已经排除了非代码文件,如图片、音频文件、演示文稿等。对于 Jupyter notebook,我们只保留了包含代码的单元格。生成的代码被存储为一个数据集,你可以在 Hugging Face Hub 上找到,位于 smangrul/hf-stack-v1。它包含仓库 id、文件路径和文件内容。

模型

我们将微调 bigcode/starcoderbase-1b 模型,这是一个在 80 多种编程语言上训练的 10 亿参数模型。这是一个需要权限的模型,所以如果你计划使用这个确切模型运行这个 notebook,你需要在其模型页面上获得访问权限。登录你的 Hugging Face 帐户以执行此操作:

[ ]

为了开始,首先让我们安装所有必要的库。正如你所看到的,除了 transformersdatasets,我们还将使用 peftbitsandbytesflash-attn 来优化训练过程。

通过采用参数高效的训练技术,我们可以在一张 A100 高内存 GPU 上运行这个 Notebook。

[ ]

现在让我们定义一些变量。请随意调整这些变量。

[ ]
[ ]

准备数据

首先加载数据。由于数据集可能相当大,请确保启用流模式。流模式允许我们在遍历数据集时逐步加载数据,而不是一次性下载数据集的整个内容。

我们将前 4000 个示例作为验证集,其余的全部作为训练数据。

[ ]

在这一步,数据集仍然包含任意长度的原始数据。为了训练,我们需要固定长度的输入。让我们创建一个可迭代的数据集,它可以从文本文件流中返回固定长度的 token 块。

首先,让我们估计数据集中每个 token 的平均字符数,这将帮助我们稍后估计文本缓冲区中的 token 数量。默认情况下,我们只从数据集中取 400 个示例(nb_examples)。只使用整个数据集的一个子集可以减少计算成本,同时仍然提供了对整体字符到 token 比的合理估计。

[ ]
100%|██████████| 400/400 [00:10<00:00, 39.87it/s] 
The character to token ratio of the dataset is: 2.43

字符到 token 的比也可以用作文本标记质量的一个指标。例如,字符到 token 的比为 1.0 意味着每个字符都由一个 token 表示,这并没有太多意义。表明标记化做得不好。在标准的英文文本中,一个 token 通常相当于大约四个字符,这意味着字符到 token 的比率大约是 4.0。我们可以预见在代码数据集中的比率会更低,但一般来说,2.0 到 3.5 之间的数字可以认为是足够好的。

可选的 FIM 变换 自回归语言模型通常是从左到右生成序列的。通过应用 FIM 变换,模型也可以学习填充文本。详细信息可以看"Efficient Training of Language Models to Fill in the Middle" 这篇论文了解这种技术。

我们将在下面定义 FIM 变换,并在创建可迭代数据集时使用它们。然而,如果你想省略变换步骤,请将 fim_rate 设置为 0。

[ ]

让我们定义 ConstantLengthDataset,这是一个可迭代的数据集,它将返回固定长度的 token 块。为此,我们将从原始数据集中读取文本缓冲区,直到达到大小限制,然后应用分词器将原始文本转换为 token 后的输入。可选项,我们可以在一些序列上执行 FIM 变换(受影响的序列比例由 fim_rate 控制)。

定义好后,我们可以从训练和验证数据中创建 ConstantLengthDataset 的实例。

[ ]

准备模型

现在数据已经准备好了,是时候加载模型了!我们将加载量化的模型。

因为量化使用更少的位来表示数据,所以会减少内存使用。我们将使用 bitsandbytes 库来量化模型,因为它与 transformers 有很好的集成。我们需要做的只是定义一个 bitsandbytes 配置,然后在加载模型时使用它。

4 比特位量化有不同的变体,但通常我们推荐使用 NF4 量化以获得更好的性能(bnb_4bit_quant_type="nf4")。

bnb_4bit_use_double_quant 选项在第一次量化后添加第二次量化,以节省每个参数额外的 0.4 位。

要了解更多关于量化的信息,请查看 "利用 bitsandbytes、4 比特位量化和 QLoRA 让 LLMs 更易于访问" 的博客

定义好后,将配置传递给 from_pretrained 方法以加载量化的模型。

[ ]

当使用量化模型进行训练时,你需要调用 prepare_model_for_kbit_training() 函数来预处理量化模型以进行训练。

[ ]

现在量化模型已经准备好了,我们可以设置一个 LoRA 配置。LoRA 通过大幅减少可训练参数的数量,使得微调更加高效。

要使用 LoRA 技术训练模型,我们需要将基础模型包装为 PeftModel。这涉及到使用 LoraConfig 定义 LoRA 配置,并使用 get_peft_model()LoraConfig 包装原始模型。

要了解更多关于 LoRA 及其参数的信息,请参考 PEFT 文档

[ ]
trainable params: 5,554,176 || all params: 1,142,761,472 || trainable%: 0.4860310866343243

可以看到,通过应用 LoRA 技术,我们现在只需要训练不到 1% 的参数。

训练模型

现在我们已经准备好了数据,并且优化了模型,我们可以将所有东西整合在一起开始训练。

要实例化一个 Trainer,你需要定义训练配置。最重要的是 TrainingArguments,这是一个包含所有用于配置训练的属性的类。

这些与你可能运行的任何其他类型的模型训练相似,所以我们这里不会详细说明。

[ ]

最后一步,实例化 Trainer 并调用 train 方法。

[ ]
Training...
TrainOutput(global_step=2000, training_loss=4.885598585128784, metrics={'train_runtime': 15380.3075, 'train_samples_per_second': 2.081, 'train_steps_per_second': 0.13, 'train_tokens_per_second': 4261.033, 'total_flos': 4.0317260660736e+17, 'train_loss': 4.885598585128784, 'epoch': 1.0})

最后,你可以将微调好的模型推送到你的 Hub 仓库中,并分享给你的团队。

[ ]

推理

一旦模型被上传到 Hub,我们就可以使用它进行推理。为此,我们首先初始化原始的基础模型及其分词器。接下来,我们需要将微调后的权重与基础模型合并。

[ ]

现在我们可以使用合并后的模型进行推理。为了方便起见,我们将定义一个 get_code_completion 函数 - 请随意尝试文本生成参数!

[ ]

现在,为了获得代码补全,我们只需要调用 get_code_complete 函数,并将我们希望补全的前几行作为前缀传递,以及一个空字符串作为后缀。

[ ]
from peft import LoraConfig, TaskType, get_peft_model
from transformers import AutoModelForCausalLM
peft_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=8,
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.1,
    bias="none",
    modules_to_save=["q_proj", "v_proj"],
    inference_mode=False,
)
model = AutoModelForCausalLM.from_pretrained("gpt2")
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()

作为刚刚在这个 notebook 中使用过 PEFT 库的人,你可以看到创建为 LoraConfig 函数的生成结果相当不错!

如果你回到我们为推理实例化模型的单元格,并注释掉我们合并微调权重的行,你可以看到原始模型对于完全相同的前缀会生成什么内容:

[ ]
from peft import LoraConfig, TaskType, get_peft_model
from transformers import AutoModelForCausalLM
peft_config = LoraConfig(
    model_name_or_path="facebook/wav2vec2-base-960h",
    num_labels=1,
    num_features=1,
    num_hidden_layers=1,
    num_attention_heads=1,
    num_hidden_layers_per_attention_head=1,
    num_attention_heads_per_hidden_layer=1,
    hidden_size=1024,
    hidden_dropout_prob=0.1,
    hidden_act="gelu",
    hidden_act_dropout_prob=0.1,
    hidden

尽管这是 Python 语法,但你可以看到原始模型并不理解 LoraConfig 应该做什么。

要了解这种高效参数微调与完全微调的比较,以及如何通过推理端点在 VS Code 中使用这样的模型作为你的编程助手(copilot),或者在本地使用,请查看"个人编程助手(copilot):训练你自己的编码助手"博客。这个 notebook 补充了原始博客内容。