赞
踩
大模型训练必然绕不开大规模分布式训练,因此本文主要阐述基于云原生架构的大规模分布式训练涉及的技术,覆盖通信、调度、存储、通信等。
本文主要借鉴了云原生机器学习平台技术综述文章,对部分内容进行了扩充,来叙述分布式训练技术在云原生上涉及的一些技术。
本文借鉴了很多网上材料,也基本都在每一节后粘贴了参考的链接,如有侵权,请联系删除。
上图为机器学习平台的普遍选型架构图,本文根据该架构图的层级进行分点阐述。从上到下,依次是:
云原生就必然绕不过kubernets,因此本章将简单阐述kubernets的一些的基本概念知识。
如果你懂Kubernets可以跳过该章节。
上图是一个简单的架构图。
控制器主要负责编排的工作,也就是kubernets最核心的功能之一。它的功能用一句话概括就是:检测期望状态和实际状态的一致性,并控制系统达到期望状态。
假设申请一个需要3副本的deployment,则处理流程如下:
kubernetes原生的kube-controller-manager由一系列的控制组成,包括EndpointController、ReplicationController、PodGCController、DaemonSetController、JobController等。因此,它只能控制原生对象,比如deployment、statefulset、service、configmap等。
正如上文所述,kubernetes原生控制器支持的资源类型有限,但是kubernetes提供了很好的扩展方式,用于用户自定义资源类型和对应的编排控制器。CoreOS开源的Operator框架提供了很好的脚手架,提高了编码效率。
Operator主要做要两件事情:
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: generateName: tfjob.kubeflow.org spec: # 资源分组 group: kubeflow.org names: # 复数 plural: tfjobs # 单数 singular: tfjob # 资源名称 kind: TFJob listKind: TFJobList scope: Namespaced versions: # 版本 - name: v1 served: true storage: true schema: ...
自定义控制器(controller):自定义controller一般以deployment的形式启动。它会监听CRD类型的资源,运行你实现的control loop逻辑,将资源拆分成pod、service、configmap等细粒度资源,应用到kubernets集群。
调度器主要完成为pod找到最适合的节点(node),然后将nodeName更新到pod的yaml上。
在scheduler的control loop中,scheduler会监听etcd的变更,拿到未被调度的pod,为pod选择node。选择node时会执行预选和优选两个策略:
可以看到,kubernets原生的scheduler调度是以POD为基本单元,通过队列进行调度的。因此,加入一个job中包含多个pod的调度,它也是以pod为基础单元调度的,因此无法满足批处理任务的调度。
每个node都会有一个kubelet,它的设计模式和控制器、调度器类似,主要工作流程如下:
资源类型原生只支持cpu和memory,但是提供厂家也可以提供device plugin进行自定义,比如说NVIDIA Device Plugin用于管理和指定GPU资源,后续也会介绍NVIDIA Device Plugin。
大规模集群训练时,容错恢复是绕不过去的话题,要做到容错恢复,那就需要checkpoint。在训练过程中,每隔一段时间保存一个checkpoint,在遇到训练中断的情况时,可以根据最近保存的检查点进行恢复。但是,大模型场景下的checkpoint的保存和恢复会对存储带来新的挑战。
以Llama2-7B
为例, 使用AdamW
优化器进行训练,保存出来的一个checkpoint大小为76GB
。由此可见,若有集群中有128
个训练实例进行70B
模型训练恢复时,会同时读取存储服务中的760GB
的checkpoint,可想而知存储的压力会很大,总数据读取量可能会有128×760GB≈97TB
。如果对进行模型切分,每个读取的checkpoint会变小,但是总数据读取量的量纲也是在TB
级别。
那么存储服务的性能会直接影响训练实例的恢复情况,影响GPU有效训练率,因此常用的基于CS架构开发的NFS是不够用的。
Minio和JuiceFS是基于云原生的分布式训练平台一个不错的存储选型。这个选型可能不是最优的,但是对于云原生的场景这个选型是兼顾性能和成本的方案。
云原生+机器学习必然绕不过对象存储,当前华为云、阿里云、百度云存储都是基于对象存储完成的。**为什么对象存储会适合云原生+机器学习场景呢?**下图是一个简单的产品架构图:
Minio是一个开源的高性能对象存储项目,下面结合Minio来说明对象存储的为什么适合云原生+机器学习平台场景?
**高扩展性:**对象存储的特性之一就是可以几乎无限扩容,正如下图Minio的分布式集群架构所示,可以在server内增加磁盘空间,也可以增加server的数量。
JuiceFS是一个开源的高性能、云原生分布式文件系统。有了Minio之后为什么还会需要JuiceFS?
下图为JuiceFS的技术架构:
从上述结构架构图,可以发现JuiceFS是对象存储和机器学习之间的一个桥梁,它有以下优势:
由上图可知,写入数据时会将数据真实存放在对象存储中,JuiceFS会维护一套元信息来保证文件系统的持久性和稳定性。
存储的选型没有最优,只有最适合。大模型分布式训练场景下,数据量猛增、对数据读写性能的要求猛增,那么用Minio+JuiceFS的方案可能就是为此而生的。Minio的高可靠性、易扩容特性解决了数据量猛增的问题,JuiceFS提供了弹性吞吐的特性。
参考:
主要讲述NVIDIA GPU硬件上如何进行互联和搭建通信渠道的。
单机或者单节点的多GPU通信,往往会用到NVLink。NVLink是不同于PCIe的通信协议,它可以直接进行GPU间的通信,绕过CPU处理,减少与通信相关的网络开销,提高计算效率。截止2024年6月,NVLink已经伴随GB200升级到第五代,GB200支持18个100GB/s带宽的NVLink,总带宽可以达到1.8TB/s。
NVSwitch是一种物理芯片,类似于交换机,可通过高速的NVLink接口连接多个GPU,提高服务器内的通信和带宽。NVSWITCH3芯片可以搭载64个NVLink4端口,提供总共12.8 Tbps的单向带宽或3.2 TB/s的双向带宽。下图为NVIDIA官网的照片。
一般的组网形式如下图所示。
分布式训练,往往会需要多机间通信,传统TCP/IP通信的效率低,因此需要RDMA(Remote Direct Memory Access,远程直接内存访问)技术来绕过CPU的频繁介入和数据拷贝,增强通信网络性能。
如上图所示,为传统的TCP/IP网络数据传输过程,可以发现数据会在各个层级内流转拷贝。
如上图所示,虚线为TCP/IP的数据传输流程,实线为RDMA传输流程。可以很直观地看出RDMA可以直接绕过CPU和多次数据拷贝,进行数据间的通信和传输。
RDMA技术的主要实现有三个:
需要注意的是,上述几种协议都需要专门的硬件(网卡)支持。
大模型背景下的分布式训练,多机多卡通信是绕不过的事情。因此为保证训练稳定性,对于硬件组网情况进行监控就必不可少。
参考:
使用kubernets组件集群,进行统一调度和编排不是难事,只需要按照教程一步步安装即可。但是原生的kubernets只能支持CPU和内存的资源分配调度,缺少对于GPU等硬件的调度。
因此,上述本章主要描述kubernetes如何去调度GPU等第三方资源,下面均以GPU为例(其实国内的训练推理加速卡都做了类似的工作)。Kubernetes涉及的技术层级图如下所示:
安装完GPU硬件后,需要安装NVIDIA驱动,然后需要安装NVIDIA Container Toolkit使得docker能够调度GPU,然后再安装NVIDIA Deivce Plugin来支持kuberenetes调度。
Kubernets的最终管理的是容器,因此如果需要在容器中使用NVIDIA GPU设备,那么NVIDIA Container Toolkit是必不可少的组件。它的主要作用是将NVIDIA GPU设备挂载到容器中。
支持docker的NVIDIA Container Tookit由如下的组件构成:
如下图所示,就是一些NVIDIA Container Toolkit的软件安装包。
安装完NVIDIA Container Toolkit后,就可以用下面这条命令进行GPU的调度
docker run -it --privileged --gpus='"device=0"' {image-id} bash
通过NVIDIA官网的图可以了解其中的调用流程
上述第三步提到的挂载文件具体可以表示为下图
其中/usr/libX/libnvidia*.so
是在驱动安装完后就可以看到的,如下图所示。
其中/dev/nvidia*
是linux下的逻辑设备文件。
正如上图所示,CUDA应用在调度的时候,其实会依赖各个不同层级的CUDA。CUDA一般分为CUDA Runtime和CUDA Driver两个版本,而挂载进去的文件/usr/libX/libnvidia*so便是CUDA Driver中的库,而一般镜像中安装的CUDA版本(比如nvidia官网提供的GPU镜像)便是CUDA Runtime版本。
Kubernets提供了device plugin,允许进行自定义设备资源类型,它的本质是一个gPRC server,Device 插件一般推荐使用 DaemonSet 的方式部署。
当 Device Plugin 向 kubelet 注册后,kubelet 就通过 RPC 与 Device Plugin 交互:
它的具体工作流程如下:
ListAndWatch
接口,上报注册节点的GPU信息和对应的DeviceID
。 nvidia.com/gpu
的GPU Pod创建出现,调度器会综合考虑GPU设备的空闲情况,将Pod调度到有充足GPU设备的节点上。 allocate
接口, 由于容器声明了GPU。 kubelet 根据之前 ListAndWatch
接口收到的Device信息,选取合适的设备,DeviceID
作为参数,调用NVIDIA Device Plugin的 Allocate
接口。NVIDIA_VISIBLE_DEVICES
环境变量,返回kubelet。 NVIDIA_VISIBLE_DEVICES
环境变量,转换为 --devices 参数,调用 nvidia-container-cli prestart。--devices
,将GPU设备映射到容器中。 并且将宿主机的Nvidia Driver Lib 的so文件
也映射到容器中。 此时容器可以通过这些so文件,调用宿主机的Nvidia Driver。根据上述可知,如果你做了以下工作:
便可以调度GPU设备了,但是你可以还需要安装DCCM exporter结合Prometheus输出GPU资源监控信息。为了方便统一安装和维护上述组件,因此NVIDIA开源了一款叫NVIDIA GPU Operator的工具。它由以下组件构成:
其中nvidia container toolkit组件镜像会自动安装工具,并挂载到宿主机目录,具体目录为** **/usr/local/nvidia
。除了上述目录,还会挂载一些别的目录,用于保证NVIDIA Container Toolkit有效性,比如:
以下为某集群安装的GPU Operator组件截图:
本节主要讲述了如何让Kubernets能够调度到GPU,需要安装哪些组件。从底层驱动到docker层的NVIDIA Container Toolkit,再到kubernets层的device plugin,以及为了统一管理的开源组件GPU Operator。
因此,在分布式训练集群搭建的时候,只需要获取设备厂家的组件安装包进行安装即可。但是组件的服务状态是需要监控的,因为这会直接影响节点是否可以调度。
参考:
Topology Manager是kubelet的一个组件,在kubernetes 1.16加入,而kubernetes 1.18中该feature变为beta版。如果要使用其实只要在启动项中加入下面参数即可(当然也可以通过配置文件指定):
--topology-manager-policy=
[none | best-effort | restricted | single-numa-node]
如果你对Topology Manager的原理不感兴趣,只需要了解为什么需要Topology Manager。
现代计算机的CPU架构多采用NUMA(Non-Uniform Memory Access,非统一内存)架构。NUMA就是将cpu资源分开,以node 为单位进行分组,每个node都有着独有的cpu、memory等资源,当一个NUMA节点内的资源相交互时,性能将会有很大的提升;但是,如果是两个NUMA节点之间的资源交互将会变得很慢。
下面这幅图中有两个NUMA节点存在:
假设某个pod需要的资源清单如下:4个CPU、200MB内存、1个GPU和1个NIC。但是由于cpu和其他外围设备(比如GPU)的分配由不同的组件完成,cpu的分配由CPU Manager完成,外围设备由Device Manager完成(正如Kubernetes使用GPU章节所述)。
因此,它们在给pod分配设备时,都是独立工作的,不会有一个全局观念。这会使调度结果很有可能是CPU和内存分配在Node0上,但是GPU却分配在Node1上。
TopologyHint是Topology Manager中一个概念。它的数据结构定义如下:
type TopologyHint struct {
NUMANodeAffinity bitmask.BitMask
Preferred bool
}
举个例子,假设有两个NUMA节点(编号分别为0和1),那么可能出现的组合为:[0]、[1]、[0,1],用bitmask表示为:01,10,11(从右往左开始,组合中有哪一个NUMA节点,那一位就是1)。
假设现在有两个NUMA节点(编号为0和1),每个NUMA节点上都有两个cpu,如果某个pod需要请求两个cpu,那么TopologyHint有如下几个:
HintProvider 是 kubelet 内部的一个组件,用于协调与 TopologyManager 对齐的资源分配。 目前 Kubernetes 中唯一的 HintProvider 是 CPUManager 和 DeviceManager。
比如NVIDIA Device Plugin会注册自己的DeviceManager。
以下是一个多种资源的TopologyHints示例。
cpu: [{01: True}, {10: True}, {11: False}]
gpu-vendor.com/gpu: [{01: True}, {10: True}]
nic-vendor.com/nic: [{01: True}, {10: True}]
上述每类资源类型生成的TopologyHints进行merge操作,并寻找最优的合并后TopologyHint。
Topology Manager提供了四种策略供用户组合各个资源的TopologyHint。这四种策略是:
如果要做千卡训练,那么集群规模会上百,那么出现跨NUMA节点分配情况的可能性会增加,因此将该策略开启,应该是个不错的选择。但是能带来多大的提升,还需要通过测试进行量化比较。
借鉴这个逻辑,大规模集群其实除了NUMA感知还可以将网络拓扑的最优子图获取亲和度调度建议,例如调度前获取整个可用资源的RoCE网络拓扑获取最优调度子图亲和度建议。
参考:
一个训练任务(job)包含了多个pod,但k8s的默认scheduler是以pod为粒度调度的,不是以job维度调度。这种简单的调度策略在深度学习模型训练场景存在一些缺陷:
Volcano(以前叫kube-batch)是华为开源的、kubernetes生态中高性能计算场景最常用的调度器,能够解决上述调度问题。
PodGroup CRD就是volcano调度的最小单位,它代表了一组强关联的pod集合。所以 VolcanoJob 背后对应一个 K8s 里的自定义控制器(Operator 模式),这个控制器会根据 VolcanoJob 的具体配置去创建相应的 PodGroup 出来。而 PodGroup 最终会被当做一个整体被 Volcano Scheduler 调度。在调度的过程中,Volcano 还用到了 Queue 来实现 PodGroup 的排队、优先级控制等逻辑。
volcano通过提供各种plugin能够解决上述提到的瓶颈:
更多可以查看https://volcano.sh/zh/docs/plugins/
volcano的强可扩展性也提供给我们很大的自由度,去定制符合业务要求的调度策略。
NUMA感知调度中提到的优化调度策略可以通过写Plugin来开发。
具体流程讲解可以参考https://bbs.huaweicloud.com/blogs/239645。
Scheduler支持动态配置和加载。左边为apiserver,右边为整个Scheduler,apiserver里有Job、Pod、Pod Group,其中Scheduler分为三部分:左边为Cache;中间层为整个调度的过程;右边是以插件形式存在的调度算法。
具体工作流程如下:
Volcano是一个开源的高性能计算场景最常用的调度器。大模型分布式训练场景下,可以通过自定义plugin来定义最适合该业务的调度策略,提升训练效率。
参考:
为什么会需要自定义资源类型(Operator)?需要先介绍常用的训练框架是如何进行分布式训练,然后再介绍kubeflow提供的training operator。
PyTorch的DDP使用了Ring AllReduce进行分布式训练,DDP依赖c10d(collective communication)库,提供了gloo、MPI、NCCL等多种通信方式。
import os import sys import tempfile import torch import torch.distributed as dist import torch.nn as nn import torch.optim as optim import torch.multiprocessing as mp from torch.nn.parallel import DistributedDataParallel as DDP def setup(rank, world_size): os.environ['MASTER_ADDR'] = 'localhost' os.environ['MASTER_PORT'] = '12355' # initialize the process group dist.init_process_group("gloo", rank=rank, world_size=world_size) def cleanup(): dist.destroy_process_group()
正如上述代码所示,PyTorch框架进行分布式训练时,每个训练节点需要获取以下几个参数:
Tensorflow的parameter server分布式训练策略从tf1.X到tf2.x都是有的,需要parameter server和worker,不同的角色会执行不同的初始化逻辑。
os.environ["TF_CONFIG"] = json.dumps({
"cluster": {
"worker": ["host1:port", "host2:port", "host3:port"],
"ps": ["host4:port", "host5:port"]
},
"task": {"type": "worker", "index": 1}
})
strategy = tf.distribute.experimental.ParameterServerStrategy(
tf.distribute.cluster_resolver.TFConfigClusterResolver(),
variable_partitioner=variable_partitioner)
coordinator = tf.distribute.experimental.coordinator.ClusterCoordinator(
strategy)
正如上述代码所示,Tensorflow框架在进行分布式训练时,需要注入TF_CONFIG的环境变量,来告知每个训练节点Master的信息、Worker的信息、以及当前节点的类型和index。
正如上述提到的,一个分布式训练任务时,所有训练框架都要求使用者通过环境变量或命令行参数传入节点地址和标识符。这些信息会用来互相发现,建立拓扑关系,从而在各个节点内实现参数的传递和同步。
但是这种操作带来诸多不便,每次启动都要传很多参数,不同节点标识符也不一样,使用MPI通信的训练框架还需要配置节点间访问秘钥,如果同时用很多机器训练模型,启动过程会非常繁琐,而这个启动流程完全是可以被自动化的。
因此,Kubeflow开源了基于operator的自定义了一些CRD,用于支持不同框架实现的分布式训练任务的编排自动化。
Kubeflow 是一种开源的 Kubernetes 原生框架,可用于开发、管理和运行机器学习(ML)工作负载。Kubeflow 是一个 AI/ML 平台,它汇集了多种工具:
更多可以查看Kubeflow。
正如上述提到的,Kubeflow Training Operator提供了一系列CRD用于分布式训练任务的自动化。具体支持如下表所示。
框架 | CRD |
---|---|
PyTorch | PyTorchJob |
TensorFlow | TFJob |
XGBoost | XGBoostJob |
MPI | MPIJob |
PaddlePaddle | PaddleJob |
apiVersion: "kubeflow.org/v1" kind: PyTorchJob metadata: name: pytorch-simple namespace: kubeflow spec: pytorchReplicaSpecs: Master: replicas: 1 restartPolicy: OnFailure template: spec: containers: - name: pytorch image: docker.io/kubeflowkatib/pytorch-mnist:v1beta1-45c5727 imagePullPolicy: Always command: - "python3" - "/opt/pytorch-mnist/mnist.py" - "--epochs=1" Worker: replicas: 1 restartPolicy: OnFailure template: spec: containers: - name: pytorch image: docker.io/kubeflowkatib/pytorch-mnist:v1beta1-45c5727 imagePullPolicy: Always command: - "python3" - "/opt/pytorch-mnist/mnist.py" - "--epochs=1"
从上述示例中,可以发现用户不需要关心master和pod一些信息,可以专注于训练本身相关的工作。
Training Operator支持将任务给volcano调度器调度。如官网所描述:在k8s中安装volcano调度器后,并为Training Operator设置一个命令行参数即可,如下所示:
spec:
containers:
- command:
- /manager
+ - --gang-scheduler-name=volcano
image: kubeflow/training-operator
name: training-operator
...
配置完上述参数后,在集群中发起新的训练任务时会自动被volcano调度。
Training Operator会监听TFJob类型的CRD并创建pod。当设置了volcano调度器时,Training Operator还做了其他事情:
因此,云原生下的分布式训练已经离不开Training Job Operator了,那么在大模型背景下,可以根据自身需要定义自己的Training Job Operator。
参考:
分布式训练策略中往往绕不过并行策略和分布式计算策略。
大模型场景下的模型规模越来越大,比如 GPT-3 模型的参数量达到1750亿。即使用1024张 80 GB 的 A100,那么完整训练 GPT-3 的时长都需要1个月,因此并行策略显得尤为重要。
知道了如何并行,但是如何去高效地实现这些并行策略,那么分布式计算便是为此而生的。
主要借鉴OneFlow的文章进行说明。
简单的机器堆叠并不一定会带来算力的增长。因为神经网络的训练并不是单纯的“把原来一个设备做的事情,现在分给多个设备各自做”,它不仅需要多个设备进行计算,还涉及到设备之间的数据传输,只有协调好集群中的计算与通信,才能做高效的分布式训练。
我们将以矩阵乘法的例子,解释数据并行、模型并行的区别。
先了解以下逻辑上的矩阵乘法例子:
假设神经网络中某一层是做矩阵乘法,其中的输入
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。