包阅导读总结
1.
关键词:飞桨框架 3.0、自动并行、大模型、分布式训练、性能优化
2.
总结:飞桨框架 3.0 为解决大模型训练难题,推出动静统一自动并行技术,降低开发难度,提升训练效率。文中介绍其架构、概念、原理、执行流程、优化策略等,未来将探索更便捷的高阶 API 接口。
3.
主要内容:
– 飞桨框架 3.0 核心升级
– 为让开发者掌握技术动态和企业高效落地,特设系列技术稿件及直播课程
– 大模型发展面临规模增长和复杂性增加带来的诸多瓶颈,分布式训练和性能优化需求迫切
– 自动并行技术方案
– 飞桨框架支持动态图手动并行和动静统一自动并行两种分布式训练方式
– 自动并行降低开发门槛,用户少量张量切分标注即可,框架自动推导和添加通信算子
– 介绍自动并行架构流程,包括用户标记和框架自动化流程
– 引入分布式张量、ProcessMesh 和 Placements 等概念及相关接口
– 举例说明自动并行框架底层执行流程和原理
– 动转静机制与性能优化
– 动转静机制实现动静框架用户标记接口和逻辑统一
– 自动并行实现大模型训练常用并行策略,并给出 8 卡混合并行示例
– 支持动态图转静态图自动并行执行,内置多种性能优化策略,如算子融合优化等
– 自动并行开发方式减少核心代码量,降低开发难度,未来将探索更便捷接口
思维导图:
文章地址:https://mp.weixin.qq.com/s/XJw4rLXuweohlvEmgPEf1A
文章来源:mp.weixin.qq.com
作者:百度Geek说
发布时间:2024/8/28 1:54
语言:中文
总字数:4386字
预计阅读时间:18分钟
评分:89分
标签:飞桨框架,自动并行,大模型训练,分布式计算,动态图
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
大模型已经成为人工智能最重要的领域之一。随着模型规模持续快速增长和模型复杂性的增加,计算瓶颈、存储瓶颈、访存瓶颈以及通信瓶颈等问题逐渐凸显。同时新的网络结构如 RWKV、Mamba 等也在不断涌现,为 AI 技术的发展注入了新的活力。为了解决这些问题,大规模分布式训练和通用性能优化的需求日益迫切。
△大模型规模快速增长
大模型往往需要使用多维混合并行方式进行训练。飞桨率先提出了四维混合并行技术,实现了数据并行、张量模型并行、流水线并行、分组参数切片并行的高效协同训练,并扩展到包括序列分片并行在内的五维混合并行,有效提升长序列输入下大模型分布式训练效率。然而,多维混合并行的开发过程往往相当复杂,开发者必须精心处理计算、通信、调度等多种逻辑,才能编写出正确的混合并行代码,这无疑提高了分布式训练开发的难度。为了解决这一难题,我们提出了动静统一自动并行的技术方案。
△飞桨框架3.0架构图
飞桨框架当前支持分布式训练当前主要有动态图手动并行和动静统一自动并行两种方式。
手动并行需要用户在开发训练代码时感知到分布式实现的细节,手动管理张量切分和通信,且不同并行策略都需要调用不同的接口,相对来说使用起来比较复杂。
自动并行为了降低用户开发分布式程序的门槛,提供了对不同分布式并行策略的统一抽象,让用户可以通过张量切分的语法标记即可实现不同并行策略。用户仅需使用少量的张量切分标注,框架便能自动推导出所有张量和算子的分布式切分状态,并添加合适的通信算子。同时自动并行还支持一键动转静分布式训练,开发者可以快速实现任意混合并行策略,大幅简化了混合并行训练代码的开发过程。
下文将主要介绍飞桨框架3.0自动并行框架中的主要概念和原理,让用户对这一全新的大模型分布式训练技术进行初步了解。
首先简要介绍一下自动并行架构的流程全貌。
用户按照单卡的逻辑视角进行模型组网,之后通过张量切分标记 API 对部分张量的切分进行语义标记 (详见下文2.1 章节),这是用户侧所需要做的所有代码开发,后续就进入框架的内部自动化流程。
框架的自动化流程主要有以下阶段:将用户标记的模型组网用分布式张量进行表示,然后进入切分推导流程为组网中的所有张量推导出一个合理高效的切分状态,接着框架的切分转换流程将为模型添加合适的通信算子 (详见下文2.2 章节),如果在静态图模式下还会基于静态图进行图优化提升训练性能 (详见下文4.1章节),最后用户可以保存或转换训练好的模型 checkpoint。
上述流程支持在动态图或静态图下执行,框架对外提供一套动静统一的接口 (详见下文2.3 章节)。下文中我们将更具体的介绍流程中的细节。
△飞桨框架3.0自动并行流程图
paddle.distributed.shard_tensor
为了描述分布张量和计算设备之间的映射关系,我们引入ProcessMesh和Placements两个分布概念。
-
ProcessMesh是指用于大模型训练或推理的多个硬件设备拓扑结构。我们将一个设备(比如一块 GPU 卡)映射为一个进程,将多个设备映射为多个进程组成的一维或多维数组,下图展示了由4个设备构成的2*2 ProcessMesh 抽象表示。 -
Placements是指表示张量在不同设备上的切分状态,分为 Replicate、Shard 和 Partial 这3种切分状态。如下图所示,Replicate 表示张量在不同设备上会以复制的形式存在;Shard 表示按照特定的维度在不同设备上进行切分;Partial 表示设备上的张量不完整,需要进行 Reduce Sum 或者 Reduce Mean 等不同方式的操作后,才能得到完整的状态。
我们用一个例子来说明ProcessMesh和Placements的语义和用法
例如,我们希望在6个计算设备上,创建一个形状为(4, 3)的分布式张量,其中沿着计算设备的 x 维,切分张量的0维;沿着计算设备的 y 维上,切分张量的1维。最终,每个计算设备实际拥有大小为(2, 1)的实际张量,如图所示。
△全局张量和分布式张量
import paddle
import paddle.distributed as dist
mesh = dist.ProcessMesh([[2, 4, 5], [0, 1, 3]], dim_names=['x', 'y'])
dense_tensor = paddle.to_tensor([[1,2,3],
[4,5,6],
[7,8,9],
[10,11,12]])
placements = [dist.Shard(0), dist.Shard(1)]
dist_tensor = dist.shard_tensor(dense_tensor, mesh, placements)
同时,为了提供重切分的能力,我们提供paddle.distributed.reshard接口,支持跨 ProcessMesh 的分布式张量转换。
例如,我们可以把在[0, 1] 两个设备上状态为Replicate的分布式张量,转换到 [2, 3] 这两个设备上,并变成状态为Shard的分布式张量。
△张量重切分示意
import paddle
import paddle.distributed as dist
mesh0 = dist.ProcessMesh([0, 1], dim_names=['x'])
mesh1 = dist.ProcessMesh([2, 3], dim_names=['x'])
dense_tensor = paddle.to_tensor([[1,2,3],
[4,5,6]])
placements0 = [dist.Replicate()]
placements1 = [dist.Shard(0)]
dist_tensor = dist.shard_tensor(dense_tensor, mesh0, placements0)
dist_tensor_after_reshard = dist.reshard(dist_tensor, mesh1, placements1)
下面我们用一个简单的列子介绍自动并行框架底层的执行流程和原理。
在单卡逻辑视角下我们希望完成计算 C = Matmul(A, B),D = Relu(C)。假设用户将 TensorB 标记成按列切分,表示在实际分布式集群中 TensorB 被按行切分到不同的 Devices 上。将 TensorA 标记成复制,表示所有 Devices 上都有完整 TensorA 副本。
import paddle
import paddle.distributed as dist
mesh = dist.ProcessMesh([0, 1], dim_names=['x'])
dense_tensorA = paddle.to_tensor([[1,2,], [3,4]])
dense_tensorB = paddle.to_tensor([[5,6], [7,8]])
placementsA = [dist.Replicate()]
placementsB = [dist.Shard(0)]
dist_tensorA = dist.shard_tensor(dense_tensorA, mesh, placementsA)
dist_tensorB = dist.shard_tensor(dense_tensorB, mesh, placementsB)
dist_tensorC = Matmul(dist_tensorA, dist_tensorB)
dist_tensorD = relu(dist_tensorC)
△切分推导之前:C 和 D 的切分状态未知
接下来就会进入自动并行的第一个核心逻辑 切分推导(InferSPMD)。当前用户标记的输入切分状态是无法被 Matmul 算子实际计算的(TensorA 的第0维和 TensorB 的第1维不匹配)。这时候自动并行框架会使用当前算子的切分推导规则(e.g. MatmulSPMD Rule),根据输入 tensors 的切分状态,推导出一套合法且性能较优的 输入-输出 张量的切分状态。
在上述输入的切分状态下,框架会推导出 TensorA 的需要按列切分,TensorB 保持切分状态不变,Matmul 的计算结果 TensorC 的切分状态是 Partial。因为后续的 Relu 算子是非线性的,输入不能是 Partial 状态,所以框架会根据 ReluSPMD Rule 将 TensorC 输入 Relu 前的的分布式状态推导成 Replicated。
△切分推导之后:C 和 D 的切分状态已知
△切分转换:执行合适的通信操作
动态图和静态图是框架的两种执行模式,动态图方便用户调试和开发,可以即时得到执行结果,静态图会做性能优化和调度编排,将硬件资源用到极致,为了兼备两者的优点,我们提供动转静机制,支持用户在动态图上开发调试后,转成静态图执行。
自动并行的 API 在设计之初,就以实现统一的用户标记接口和逻辑为目标,保证动静半框架保证在相同的用户标记下,动静态图分布式执行逻辑一致。这样用户在全流程过程中只需要标记一套动态图组网,即可以实现动态图下的分布式训练 Debug 和 静态图下的分布式推理等逻辑。整个动转静训练的逻辑如下:
△动静统一
...
dist_model = dist.to_static(
model, dataloader, paddle.mean, opt
)
dist_model.train()
for step, inputs in enumerate(dataloader()):
data = inputs
loss = dist_model(data)
print(step, loss)
基于上述介绍的框架,飞桨框架3.0自动并行已经能够实现大模型训练过程中常用的并行策略。
下面是一个完整的包含数据并行、张量并行、流水并行三种策略的示例。计算设备一共有8张 GPU 卡,编号分别为0-7,他们组成两个 ProcessMesh,分布为[[0,1],[2,3]]和[[4,5],[6,7]]。模型由两个 matmul 算子组成,两个 matmul 分别在 两个不同的 mesh 上,中间结果需要跨mesh传输,做流水线并行;输入数据在ProcessMesh的0维被切分,做数据并行;两个 matmul 的参数在 ProcessMesh 的 1 维上分别被按列或按行切分,做张量模型并行。如下图所示:
△8卡混合并行示例
import paddle
import paddle.distributed as dist
from paddle.io import BatchSampler, DataLoader, Dataset
import numpy as np
mesh0 = dist.ProcessMesh([[0, 1], [2, 3]], dim_names=['x', 'y'])
mesh1 = dist.ProcessMesh([[4, 5], [6, 7]], dim_names=['x', 'y'])
class RandomDataset(Dataset):
def __init__(self, seq_len, hidden, num_samples=100):
super().__init__()
self.seq_len = seq_len
self.hidden = hidden
self.num_samples = num_samples
def __getitem__(self, index):
input = np.random.uniform(size=[self.seq_len, self.hidden]).astype("float32")
label = np.random.uniform(size=[self.seq_len, self.hidden]).astype("float32")
return input, label
def __len__(self):
return self.num_samples
class MlpModel(paddle.nn.Layer):
def __init__(self):
super(MlpModel, self).__init__()
self.w0 = dist.shard_tensor(
self.create_parameter(shape=[1024, 4096]),
mesh0, [dist.Replicate(), dist.Shard(1)])
self.w1 = dist.shard_tensor(
self.create_parameter(shape=[4096, 1024]),
mesh1, [dist.Replicate(), dist.Shard(0)])
def forward(self, x):
y = paddle.matmul(x, self.w0)
y = dist.reshard(y, mesh1, [dist.Shard(0), dist.Shard(2)])
z = paddle.matmul(y, self.w1)
return z
model = MlpModel()
dataset = RandomDataset(128, 1024)
sampler = BatchSampler(
dataset,
batch_size=2,
)
dataloader = DataLoader(
dataset,
batch_sampler=sampler,
)
dataloader = dist.shard_dataloader(dataloader, meshes=[mesh0, mesh1], shard_dims='x')
opt = paddle.optimizer.AdamW(learning_rate=0.001, parameters=model.parameters())
opt = dist.shard_optimizer(opt)
for step, inputs in enumerate(dataloader()):
data = inputs[0]
logits = model(data)
loss = paddle.mean(logits)
loss.backward()
opt.step()
opt.clear_grad()
在实际的大模型业务训练中,训练吞吐是一个非常重要的指标。飞桨框架3.0支持在动态图自动并行转为静态图自动并行执行,并自动添加多种性能优化策略,支持计算图全局优化,这也是飞桨框架3.0动静统一自动并行的一大特色。自动并行内置实现了多种性能优化策略,包括:算子融合优化、流水线编排调度方式、高效-计算Overlap、通信融合优化等,用于来提升分布式训练吞吐。用户在可以通过简单的以下接口中配置相关选项,即可开启静动态图自动并行性能优化策略:paddle.distributed.Strategy。
例如,在常见的大语言模型中,matmul 和 add 可以进行算子融合 fusion,减少中间变量的访存开销。在自动并行模式下,可以通过如下代码进行开启:
import paddle
import paddle.distributed as dist
strategy = dist.Strategy()
strategy.fused_passes.enable = True
strategy.fused_passes.gemm_epilogue = True
比如在流水线并行下,通过 strategy 设置1F1B 和 virtual-pipeline 的调度方式可以实现的interleaved流水线编排调度,减少流水线执行时的 bubble 开销。
△Interleaved 流水线调度图
from paddle.distributed as dist
strategy = dist.Strategy()
pipeline = strategy.pipeline
pipeline.enable = True
pipeline.schedule_mode = "1F1B"
pipeline.vpp_degree = 2
飞桨框架3.0版本下,通过采用自动并行的开发方式,开发者无需再考虑复杂的通信逻辑。开发者无需深入研究手动并行编程的复杂概念和 API,只需进行少量的张量切分标注,即可完成混合并行模型的构建。其分布式训练核心代码量减少了 50%,从而大大降低了开发的难度!
△手动并行和自动并行代码对比
飞桨框架3.0向用户提供了一种动静统一的大模型开发范式,在动态图自动并行上,用户可以方便的打印中间结果调试验证自己的算法模型;调试完成后,通过飞桨框架3.0动转静机制将模型转成静态图,静态图自动并行框架会应用各种分布式优化 Pass 策略,提升模型分布式训练吞吐。
未来,我们将进一步探索和开发无需用户进行张量切分标记的自动并行高阶 API 接口,让开发者可以像单卡训练一样只需要关心模型算法,并通过一个简单的接口自动实现大模型分布式训练,进一步提升大模型的开发体验。