使用 Kubernetes 和 Intel® Xeon® 可扩展处理器微调 LLM

社区文章 发布于 2024 年 4 月 24 日

引言

用于文本生成的大型语言模型(LLM)已广受欢迎,但由于模型和数据集规模庞大,训练这些模型需要巨大的计算能力,这已不是秘密。此前,我们已经看到,当与软件优化(如 Intel® Extension for PyTorchIntel® oneCCL Bindings for PyTorch)结合使用时,多个 CPU 的效率有多高。Kubernetes (简称 K8s) 编排跨节点集群运行容器化工作负载。在这篇博客中,我们将深入探讨使用 K8s 集群中的多个 Intel® Xeon® 可扩展处理器节点,对 meta-llama/Llama-2-7b-hf 模型使用 medalpaca/medical_meadow_medical_flashcards 数据集进行微调的过程。

目录

为什么选择 Kubernetes?

K8s 编排让我想起戈登·拉姆齐大厨在《地狱厨房》里大喊命令——团队协同合作以完成接到的订单。有些订单发给红队,有些发给蓝队。像沙拉这样简单的菜可能只需要一个人快速完成,而一道复杂的主菜可能需要多人参与。类似地,K8s 有一个调度器,根据节点的可用性和应用程序所需的资源,将任务分配给集群中的节点。在任何给定时间,单个节点可能运行多个不同的任务,也可能运行一个消耗其所有资源的单个任务。如果集群中的所有节点都忙,则任务将处于待处理状态,直到节点资源可用。

对意外问题的处理也可以与 K8s 进行比较。如果有人烧伤了手被送去治疗,团队的其他人会继续处理订单。类似地,如果 K8s 集群中的一个节点宕机,集群的其余部分会继续运行。此外,如果厨房里的某个人牛排煮得过熟,他们就需要重做。如果在 K8s pod 中发生故障,它会重新启动。

image/png

当有大量任务部署到集群时(你在家做通心粉和奶酪时不需要戈登·拉姆齐大厨),K8s 会变得特别有用。这可能是一个工程师团队共享一个节点集群,或者一大堆多节点实验要运行,或者一个处理大量请求的生产集群。在所有这些情况下,Kubernetes 控制平面管理集群资源并协调哪个节点将用于运行每个 pod。

组件

在本教程中,我们将使用多个 CPU 节点对 Llama 2 进行微调,并使用 Hugging Face 数据集。运行此任务涉及到几个不同的组件。下图展示了这些组件之间的交互。

image/png

Helm Chart

我们要谈的第一个组件是 Helm charts。这就像我们任务的“食谱”。它将用于我们任务的所有不同组件聚集在一起,并允许我们使用一个 helm install 命令部署所有内容。我们示例中使用的 K8s 资源是:

  • 用于模型微调的 PyTorchJob,带有多个工作节点
  • 用作数据集和模型文件在工作节点之间共享存储位置的持久卷声明 (PVC)
  • 用于受限模型的 Secret(可选)
  • 数据访问 Pod(可选)

Helm chart 有一个 values.yaml 文件,其中包含用于 K8s 资源规范文件的参数。我们的 values 文件包含 K8s 资源的名称、用于工作节点 pod 的容器的镜像/标签、工作节点数量、CPU 和内存资源、Python 脚本的参数等。当 Helm chart 部署时,这些值会被填充到 K8s 规范文件中。

容器

K8s 在容器化环境中运行任务,所以我们接下来需要一个 Docker 容器。你可以把它想象成一个搅拌碗。在我们的搅拌碗中,我们需要包含训练任务所需的所有依赖项。为了在使用 Intel Xeon 处理器时获得最佳性能,我们建议包含 Intel® Extension for PyTorchIntel® oneCCL Bindings for PyTorch

在这个例子中,我们将使用 DockerHub 中的 `intel/ai-workflows:torch-2.2.0-huggingface-multinode-py3.10` 镜像,它包含以下包:

包名 版本 目的
PyTorch 2.2.0 训练模型的基础框架
🤗 Transformers 4.39.3 用于下载和微调 Hugging Face 模型的库
Intel® Extension for PyTorch 2.2.0 扩展 PyTorch 以在 Intel® 硬件上提供额外的性能提升
Intel® oneAPI Collective Communications Library 2.2.0 在多个节点上部署 PyTorch 任务

Dockerfile 中包含的其他关键内容有 MKL、google-perftools🤗 PEFT🤗 Datasets 以及 OpenSSH,以允许 Intel oneAPI CCL 在容器之间进行通信。

微调脚本

我们使用的 Python 脚本用于微调文本生成的因果语言模型。它的参数与您在 Hugging Face 示例脚本中看到的类似。脚本中的所有参数都与 values.yaml 中的参数相同,只是转换为驼峰命名法以匹配 Helm 命名约定。此脚本可用于微调聊天机器人或进行指令微调。

基于聊天的模型将使用以下提示格式:

<s>[INST] <<SYS>>
Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.<</SYS>>

Calculate the median of the following list of integers. 6, 5, 8, 1, 2, 1, 7 [/INST] 5 </s>

其他模型将使用以下提示格式进行指令微调:

Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
Calculate the median of the following list of integers.

### Input:
6, 5, 8, 1, 2, 1, 7

### Response:
5

提示字符串也可以使用脚本参数进行自定义,以便提供更适合您任务的提示。例如,您可能希望提供一个更具针对性的提示,例如“您是一位乐于助人、尊重他人且诚实的财务助理。请始终尽可能提供有帮助的答案,同时确保安全。您的答案不应包含任何有害、不道德、种族主义、性别歧视、有毒、危险或非法内容。请确保您的回答具有社会公正性且积极向上。如果一个问题没有任何意义或事实不连贯,请解释原因,而不是给出不正确的答案。如果您不知道问题的答案,请不要分享虚假信息。”

Python 脚本需要包含在 Docker 容器中,所以我们把它添加到我们的“搅拌碗”里。我们的 Dockerfile 使用 COPY 命令将脚本添加到镜像中。

存储

我们需要一个可以在工作节点之间共享的存储位置,用于访问数据集和保存模型文件。我们正在使用一个带有 NFS 支持的存储类的普通 K8s 集群。如果您使用的是云服务提供商,则可以使用云存储桶代替。

回顾我们的烹饪类比,存储可以比作储藏室或冰箱。你不会把整个储藏室都放进搅拌碗里,同样地,我们的 NFS 存储位置也不会添加到容器中。相反,存储位置会挂载到容器中,这样我们就可以访问该位置进行读写,而无需将其构建到镜像中。为了实现这一点,我们正在使用一个持久卷声明 (PVC)

Secret

我们添加的最后一个成分是秘密酱。受限模型或私有模型要求您登录才能下载模型。为了从 K8s 任务进行身份验证,我们使用Hugging Face 用户只读访问令牌定义了一个秘密。来自秘密的令牌将被挂载到容器中。如果正在训练的模型不是受限或私有的,则不需要此操作。

集群要求

本教程要求在您的集群上安装 Kubeflow。Kubeflow 提供了一些特性和自定义资源,可以简化在 K8s 集群上运行和扩展机器学习工作负载。在此示例中,我们将使用 Kubeflow 的PyTorch 训练操作器。PyTorch 训练操作器允许我们在集群上运行分布式 PyTorch 训练任务,而无需手动设置 MASTER_ADDRMASTER_PORTRANKWORLD_SIZE 等环境变量。安装 Kubeflow 后,请使用 kubectl get crd pytorchjobs.kubeflow.org 验证 PyTorch 自定义资源是否已成功部署到您的集群,并确保输出类似于:

NAME                       CREATED AT
pytorchjobs.kubeflow.org   2023-03-24T15:42:17Z

我们的集群使用第四代 Intel® Xeon® 可扩展处理器,以利用 Intel® Advanced Matrix Extensions (Intel® AMX) 和 bfloat16。如果您的集群启用了基于角色的访问控制 (RBAC),则列出节点和许多其他集群范围的命令需要向用户授予特定角色。kubectl auth can-i get nodes 命令如果能够使用 kubectl get nodes 列出节点,则会返回“yes”,例如:

NAME                 STATUS     ROLES                      AGE   VERSION
k8s-spr-01           Ready      worker                     69d   v1.22.17
k8s-spr-02           Ready      worker                     68d   v1.22.17
k8s-spr-03           Ready      worker                     65d   v1.22.17
k8s-spr-04           Ready      worker                     65d   v1.22.17

否则,请咨询您的集群管理员,获取您用户组可用的节点列表。

一旦您知道节点的名称,请使用 kubectl describe node <node name> 命令获取其 CPU 和内存容量。我们将在后面设置工作节点 pod 规范时使用此信息。

教程:使用 Kubernetes 集群微调 Llama 2

客户端要求

  • 已安装 kubectl 并配置连接到您的集群
  • Helm
  • 下载并解压本教程中使用的 Helm chart
    wget https://storage.googleapis.com/public-artifacts/helm_charts/tlt_v0.7.0_hf_helm_chart.tar.gz
    tar -xvzf tlt_v0.7.0_hf_helm_chart.tar.gz
    
    这个 tar 文件包含了 3 个不同的 Helm 值文件示例
    • hf_k8s/chart/values.yaml:运行您自己工作负载的模板
    • hf_k8s/chart/medical_meadow_values.yaml:使用 medalpaca/medical_meadow_medical_flashcards 数据集微调 Llama 2
    • hf_k8s/chart/financial_chatbot_values.yaml:使用金融数据集的子集微调 Llama 2 聊天机器人。选择其中一个作为以下说明中的值文件。

步骤 1:使用 Hugging Face 令牌设置 Secret

获取一个具有读取权限的 Hugging Face 令牌,并使用您的终端通过 echo <您的令牌> | base64 获取您令牌的 base64 编码。

例如:

$ echo hf_ABCDEFG | base64
aGZfQUJDREVGRwo=

复制并粘贴编码后的令牌值到你的 values YAML 文件中 secret 部分的 encodedToken 字段。例如,要运行 Medical Meadow 微调任务,打开 hf_k8s/chart/medical_meadow_values.yaml 文件,在第 23 行粘贴你的编码令牌。

secret:
  encodedToken: aGZfQUJDREVGRwo=

步骤 2:自定义 values.yaml 参数

hf_k8s/chart/medical_meadow_values.yaml 文件已设置为使用 medalpaca/medical_meadow_medical_flashcards 数据集微调 meta-llama/Llama-2-7b-hf。如果您正在使用 hf_k8s/chart/values.yaml 模板,请填写 datasetName 以使用 Hugging Face 数据集,或者提供 dataFile 路径。

文件的 distributed.train 部分可以更改以调整训练任务的数据集、 epoch、最大步数、学习率、LoRA 配置、启用 bfloat16 等。

values 文件还包含用于设置 pod 安全上下文的参数,其中包含您的用户和组 ID,以允许以非 root 用户身份运行微调脚本。如果未设置用户和组 ID,它将以 root 身份运行。

securityContext:
  runAsUser:
  runAsGroup:
  fsGroup:
  allowPrivilegeEscalation: false

values.yaml 文件中还有其他需要根据您的集群进行配置的参数

elasticPolicy:
  rdzvBackend: c10d
  minReplicas: 1
  maxReplicas: 4  # Must be greater than or equal to the number of distributed.workers
  maxRestarts: 30

distributed:
  workers: 4

...

# Resources allocated to each worker
resources:
  cpuRequest: 200                 # Update based on your hardware config
  cpuLimit: 200
  memoryRequest: 226Gi
  memoryLimit: 226Gi
  nodeSelectorLabel: node-type    # Update with your node label/value
  nodeSelectorValue: spr

# Persistent volume claim storage resources
storage:
  storageClassName: nfs-client     # Update with your cluster's storage class name
  resources: 50Gi
  pvcMountPath: /tmp/pvc-mount

YAML 中定义的 CPU 资源限制/请求以CPU 单位表示,其中 1 个 CPU 单位等同于 1 个物理 CPU 核心或 1 个虚拟核心(取决于节点是物理主机还是虚拟机)。YAML 中定义的 CPU 和内存限制/请求量应小于单个机器上可用的 CPU/内存容量。通常,最好不要使用整个机器的容量,以便为 kubelet 和操作系统留出一些资源。为了为工作节点 Pod 获取“保证”服务质量,请为请求和限制设置相同的 CPU 和内存量。

步骤 3:将 Helm chart 部署到集群

使用 kubeflow 命名空间将 Helm chart 部署到集群

# Navigate to the hf_k8s directory from the extracted tar file
cd hf_k8s

# Deploy the job using the Helm chart, specifying your values file name with the -f parameter
helm install --namespace kubeflow -f chart/medical_meadow_values.yaml llama2-distributed ./chart

步骤 4:监控任务

Helm chart 部署到集群后,K8s 资源(如 Secret、PVC 和工作节点 Pod)将被创建。可以通过使用 kubectl get pods 查看 Pod 状态来监控任务。最初,Pod 将显示为“Pending”,因为容器正在拉取和创建,然后状态应更改为“Running”。

$ kubectl get pods
NAME                                                READY   STATUS             RESTARTS         AGE
medical-meadow-dataaccess                           1/1     Running            0                1h30m
medical-meadow-pytorchjob-worker-0                  1/1     Running            0                1h30m
medical-meadow-pytorchjob-worker-1                  1/1     Running            0                1h30m
medical-meadow-pytorchjob-worker-2                  1/1     Running            0                1h30m
medical-meadow-pytorchjob-worker-3                  1/1     Running            0                1h30m

使用 kubectl logs <pod name> 查看训练日志。您还可以添加 -f 以流式传输日志。

$ kubectl logs medical-meadow-pytorchjob-worker-0
...
72%|███████▏  | 2595/3597 [4:08:05<1{'loss': 2.2737, 'learning_rate': 5.543508479288296e-05, 'epoch': 2.17}
...

步骤 5:下载训练好的模型

任务完成后,可以使用以下命令将训练好的模型从 /tmp/pvc-mount/output/saved_model(您在 values 文件中为 train.outputDir 参数定义的路径)复制到本地系统。

kubectl cp --namespace kubeflow medical-meadow-dataaccess:/tmp/pvc-mount/output/saved_model .

步骤 6:清理

最后,可以使用 helm uninstall 命令并指定 Helm 任务的名称,从集群中删除资源。可以使用 helm list 命令查看所有已部署的 Helm 版本。

helm uninstall --namespace kubeflow llama2-distributed

卸载 Helm chart 后,集群上的资源将显示“Terminating”状态,然后最终消失。

结果

我们使用 Medical Meadow Flashcards 数据集,在我们的普通 k8s 集群上,利用第四代 Intel® Xeon® 可扩展处理器,对 Llama2-7b 进行了 3 个 epoch 的微调。我们使用了 values.yaml 文件中的默认参数,然后尝试更改工作节点数量并开关混合精度训练。文本生成模型的准确性衡量可能很棘手,因为没有明确的正确或错误答案。相反,我们使用困惑度指标粗略衡量模型对其预测的置信度。我们保留了 20% 的数据用于评估。我们发现,增加工作节点数量可显著缩短训练时间。当从单个节点增加到 2 个节点和 4 个节点时,您不会看到精确的 2 倍和 4 倍改进,因为节点之间存在通信开销,并且某些资源会保留给操作系统。我们还发现,使用 BFloat16 训练而不是 FP32 可显著提高性能。在所有情况下,困惑度值都保持相对一致。这是一个好现象,因为有时模型在扩展任务或使用较少比特(如 BFloat16)进行训练时,准确性可能会下降。

下一步

既然我们已经使用 Medical Meadow Flashcards 数据集对 Llama 2 进行了微调,您可能想知道如何利用这些信息在 K8s 上运行自己的工作负载。如果您正在使用 Llama 2 或类似的生成式文本模型,那么您很幸运,因为可以重复使用相同的 Docker 容器和脚本。您只需编辑 values.yaml 文件中 distributed.train 部分的参数,以使用您的数据集并根据需要调整其他参数(学习率、epochs 等)。如果您想使用自己的微调脚本,您将需要构建一个 Docker 容器,其中包含运行训练任务所需的库以及您的脚本。该镜像需要复制到集群节点或推送到容器注册表。容器镜像和标签需要更新到 values.yaml 文件中,同时更新脚本名称和脚本的所有 Python 参数。

本教程的所有脚本、Dockerfile 和规范文件都可以在我们的 GitHub 仓库中找到。

Intel® AI Containers for PyTorch 基本容器可用于运行您自己的分布式训练任务。

有关分布式训练的其他资源,请查阅 Hugging Face 文档中关于在多个 CPU 上进行高效训练的部分。

致谢

感谢我的同事们为这篇博客做出的贡献和帮助审阅:Harsha Ramayanam, Omar Khleif, Abolfazl Shahbazi, Rajesh Poornachandran, Melanie Buehler, Daniel De Leon, Tyler Wilbers, 和 Matthew Fleetwood。

参考

@misc{touvron2023llama,
      title={Llama 2: Open Foundation and Fine-Tuned Chat Models},
      author={Hugo Touvron and Louis Martin and Kevin Stone and Peter Albert and Amjad Almahairi and Yasmine Babaei and Nikolay Bashlykov and Soumya Batra and Prajjwal Bhargava and Shruti Bhosale and Dan Bikel and Lukas Blecher and Cristian Canton Ferrer and Moya Chen and Guillem Cucurull and David Esiobu and Jude Fernandes and Jeremy Fu and Wenyin Fu and Brian Fuller and Cynthia Gao and Vedanuj Goswami and Naman Goyal and Anthony Hartshorn and Saghar Hosseini and Rui Hou and Hakan Inan and Marcin Kardas and Viktor Kerkez and Madian Khabsa and Isabel Kloumann and Artem Korenev and Punit Singh Koura and Marie-Anne Lachaux and Thibaut Lavril and Jenya Lee and Diana Liskovich and Yinghai Lu and Yuning Mao and Xavier Martinet and Todor Mihaylov and Pushkar Mishra and Igor Molybog and Yixin Nie and Andrew Poulton and Jeremy Reizenstein and Rashi Rungta and Kalyan Saladi and Alan Schelten and Ruan Silva and Eric Michael Smith and Ranjan Subramanian and Xiaoqing Ellen Tan and Binh Tang and Ross Taylor and Adina Williams and Jian Xiang Kuan and Puxin Xu and Zheng Yan and Iliyan Zarov and Yuchen Zhang and Angela Fan and Melanie Kambadur and Sharan Narang and Aurelien Rodriguez and Robert Stojnic and Sergey Edunov and Thomas Scialom},
      year={2023},
      eprint={2307.09288},
      archivePrefix={arXiv},
      primaryClass={cs.CL}
}

@article{han2023medalpaca,
  title={MedAlpaca--An Open-Source Collection of Medical Conversational AI Models and Training Data},
  author={Han, Tianyu and Adams, Lisa C and Papaioannou, Jens-Michalis and Grundmann, Paul and Oberhauser, Tom and L{\"o}ser, Alexander and Truhn, Daniel and Bressem, Keno K},
  journal={arXiv preprint arXiv:2304.08247},
  year={2023}
}

社区

注册登录 发表评论