[分布式并行策略] 数据并行 DP/DDP/FSDP/ZeRO

上篇文章【[论文品鉴] DeepSeek V3 最新论文 之 DeepEP】 介绍了分布式并行策略中的EP,简单的提到了其他几种并行策略,但碍于精力和篇幅限制决定将内容分几期,本期首先介绍DP,但并不是因为DP简单,相反DP的水也很深,例如:“DP到底同步的是什么数据?怎么同步的?“,“AllReduce/Ring-AllReduce是什么?”,“ZeRO1、2、3又都是什么?” 等各种问题,会结合PyTorch代码,尽量做到详细由浅入深。

单机单卡

在深入分布式并行策略前,先回顾一下单机单卡的训练模式:

在这里插入图片描述

  1. CPU 加载数据,并将数据分成 batch 批次
  2. CPU 将 batch 批次数据传给 GPU
  3. GPU 进行 前向传播 计算得到 loss
  4. GPU 再通过 反向传播 通过 loss 得到 梯度
  5. GPU 再通过 梯度 更新 参数

伪代码:

model = Model(xx) # 1. 模型初始化
optmizer = Optimizer(xx) # 2. 优化器初始化
output = model(input) # 3. 模型计算
loss = loss_function(output, target) # 4. loss计算
loss.backward() # 5. 反向传播计算梯度
optimizer.step() # 6. 优化器更新参数 

DP

就像 多线程编程 一样,可以通过引入 多个GPU 来提高训练效率,这就引出了最基础的 单机多卡 DP,即 Data Parallel 数据并行。

在这里插入图片描述

  1. CPU 加载数据,并将数据拆分,分给不同的 GPU
  2. GPU0 将 模型 复制到 其他所有 GPU
  3. 每块 GPU 独立的进行 前向传播 和 反向传播 得到 梯度
  4. 其余所有 GPU 把 梯度 传给 GPU0
  5. GPU0 汇总全部 梯度 进行 全局平均 计算
  6. GPU0 通过 全局平均 的 梯度 更新自己的 模型
  7. GPU0 再把最新的 模型 同步到其他 GPU

DP的PyTorch伪代码,相比于单机单卡,大部分都没有变化,只是把模型换了DataParallel模型,在PyTorch中通过nn.DataParallel(module, device_ids) 实现:

model = Model(xx) # 1. 模型初始化(没变化)
model_new = torch.nn.DataParallel(model, device_ids=[0,1,2]) # 1.1 启用DP (新增)
optmizer = Optimizer(xx) # 2. 优化器初始化(不变)
output = model_new(input) # 3. 模型计算,替换使用DP(变更)
loss = loss_function(output, target) # 4. loss计算(不变)
loss.backward() # 5. 反向传播计算梯度(不变)
optimizer.step() # 6. 优化器更新参数 (不变)

可见DP使用上非常简单,通过nn.DataParallel套上之前的模型即可。

但是DP存在 2个 比较严重的问题:

  1. 数据传输量较大:不考虑CPU将input数据拆分传输给每块GPU,单独看GPU间的数据传递;对于GPU0它需要把整个模型的参数广播到其他所有GPU,假设有 N N N块GPU,那么就需要传输 ( N − 1 ) ∗ w (N-1)*w (N−1)∗w参数,同时GPU0也需要从其他所有GPU上Reduce所有梯度,那么就要传输 ( N − 1 ) ∗ g (N-1)*g (N−1)∗g,所以对于GPU0来说要传输 ( N − 1 ) ∗ ( w + g ) (N-1)*(w +g) (N−1)∗(w+g)的数据,同理对于其他GPU来说,要传输与来自GPU0的参数,与传出自己那份梯度。所以整体上个说,GPU数量多 N N N越大,传输的数据量就越多。
  2. GPU0的压力太大:它要收集梯度、更新参数、同步参数,计算和通信压力都很大

接下来看一下更高级用法 DDP

DDP

DDP 即 Distributed Data Parallel,多机多卡的分布式数据并行。

在这里插入图片描述
与 DP 最主要的区别就是,解决了 DP 的 主节点瓶颈,实现了真正的 分布式通信。

而精髓就是 Ring-AllReduce,下面介绍它是如何实现 梯度累计 的:

  1. 假设梯度目前都是单独存在于不同GPU上,而目标是将三个GPU的梯度进行累计,也就是得到下图中三个梯度的和,a0+a1+a2 与 b0+b1+b2和c0+c1+c2
    在这里插入图片描述
  2. 首先第一阶段:GPU0将a0发送给GPU1去求和a0+a1,GPU1将b1发送给GPU2去求和b1+b2,GPU2将c2发送给GPU0去求和c0+c2
    在这里插入图片描述
  3. 然后,继续累加,将GPU0上的c0+c2发送给GPU1去求c0+c1+c2,GPU1将a0+a1发送给GPU2去求a0+a1+a2,将GPU2将b1+b2发送给GPU0去求b0+b1+b2
    在这里插入图片描述
  4. 此时第一阶段完成,通过Scatter-Reduce将参数分发后集合,分别得到了各个参数梯度累计结果
    在这里插入图片描述
  5. 之后的第二节阶段,通过All-Gather将各个参数的梯度进行传播,使得每个GPU上都得到了完整的梯度结果
  6. 首先,GPU0将完整的b0+b1+b2传递给GPU1,同理GPU1和GPU2也传递完整的梯度
    在这里插入图片描述
  7. 最后,再将剩余的梯度进行传递
    在这里插入图片描述
  8. 最终每个设备得到了所有参数的完整梯度累计
    在这里插入图片描述

在 DDP 的Ring-AllReduce 中还有一个细节:如果每个参数都这么Ring着进行信息梯度累计,那么通信压力太大了;

所以设计了桶,通过将参数分桶聚合,也就是一个桶中维护了多个参数,当整个桶中的所有梯度都计算完毕后,再以桶维度进行Ring的梯度累计,这样降低了通信压力,提高了训练效率。

DDP的落地,相较于DP会复杂很多,首先简单理解几个概念:

  • world:代表着DDP集群中的那些卡的
  • rank:world中,每张卡的唯一标识
  • nccl、gloo:都是通信库,也就是那些分布式原语的实现,现在普遍都用老黄家的NCCL,搭配RMDA食用效率更高

接下来看一下DDP的PyTorch伪代码:

# 首先需要在每张卡,也就是进程单位设置一下,可以理解为在“组网” (新增)
import torch
import torch.distributed as dist
dist.init_process_group(backend = "nccl", # 使用NCCL通信rank = xx, # 这张卡的标识world_size = xx # 所有卡的数量
)
torch.cuda.set_device(rank) # 绑定这个进程的GPU# 然后是模型定义(变化)
model = Model(xx).cuda(rank)
model_ddp = nn.parallel.DistributedDataParallel(mode, device_ids=[rank]) # 相较于DP,这里用DDP来包装模型# 优化器(没变)
optimizer = Optimizer(xx)# 分布式数据加载(新增)
train_sampler = torch.utils.data.distributed.DsitributedSampler(dataset,num_replicas = world_size,rank = rank
)
dataloader = DataLoader(dataset,batch_size = per_gpu_batch_size,sampler = train_sampler
)# 训练(不变)
output = model_ddp(input)
loss = loss_function(output, target)
loss.backward()
optimizer.step()# 训练后结束"组网"
dist.destroy_process_group()# 使用torchrun启动DDP
torchrun train.py # torchrun是pytorch官方DDP的最佳实践,就别用其他的了

FSDP

不论是DP还是DDP的数据并行,都有一个核心问题:模型在每个GPU上都存储一份,如果模型特别大,单卡显存不足的话就无法训练。

这就引入了 FSDP(fully sharded data parallel)核心思想是:把模型的参数、梯度、优化器状态 分片存储,显著降低显存占用。

分片机制:

  • 参数分片:把模型的参数切分到所有GPU上,每个GPU仅存储部分参数
  • 前向传播:通过 AllGather 收集完整参数 -> 计算 -> 丢弃 非本地分片(不在显存中存储,仅仅是计算用)
  • 反向传播:通过 AllGather 收集参数 -> 计算梯度 -> 再通过 reduce-scatter同步梯度分片
  • 优化器状态:每个GPU仅维护与其参数分片对应的优化器状态

但这时候就会有疑问了:把模型分片存储,这还算DP吗,这不成了MP么?

确实,FSDP融合DP和MP两种思想,但核心仍然是DP,因为它仍然是在 数据维度 进行并行(不同GPU处理不同数据),并且每个GPU都独立的完整前向+反向传播;这是用DP的思想,去解决DP单卡显存瓶颈的问题。

“FSDP is DP with model sharding, not MP. It extends DP beyond single-device memory limits.”
—— PyTorch Distributed Team, Meta AI

下面展示FSDP的FULLY_SHARD策略,也就是对标ZeRO-3的训练流程:

  1. 通过FULLY_SHARD策略,将参数、梯度、优化器状态进行了分片
    在这里插入图片描述
  2. 在前向传播中,由于每个GPU都只有部分参数,所以当走到缺失那部分参数的时候,依赖其他GPU将参数传进来,执行完毕后就丢弃;通过这种方式,使得即使每个GPU只保存部分参数,但依然可以完成整个前向传播
    在这里插入图片描述
  3. 当得到output开始计算梯度时,每个GPU完整自己那部分的梯度计算,在此过程中如果本地没有相对应的参数,也依然需要从其他GPU传过来;当完成梯度计算后,再把梯度发送给负责更新这部分参数的优化器分片的GPU,由它进行本地参数更新;这样就完成了一次前向+反向传播
    在这里插入图片描述

再来看看FSDP的PyTorch伪代码:

# “组网”,也就是设置分布式环境方式和DDP没有区别(不变)
import torch.distributed as dist
from torch.distrbuted.fsdp import FullyShardDataParallel as FSDP
def setup(rank, world_size): dis.init_process_group("nccl", rank=rank, world_size=world_size)torch.cuda.set_device(rank)# 使用FSDP包装模型,同时设置分片策略(新增)
from torch.distributed.fsdp.wrap import size_based_auto_wrap_policy
model = Model(xx)
model_fsdp = FSDP(mode, auto_wrap_policy=size_based_auto_wrap_policy, # 按层大小自动分片mixed_precision=True, # 启用混合精度device_id=rank,sharding_strategy=torch.distributed.ShardingStrategy.FULLY_SHARD # 相当于ZeRO-3
)# 数据加载和分布式采样,和DDP没有区别(不变)
from torch.utils.data.distributed import DistributedSampler
dataset = datasets(xx)
sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank)
dataloader == torch.utils.data.DataLoader(datase4t, batch_size=64, sampler=sampler)# 训练和DP、DDP没有区别(不变)
for epoch in range(epochs):sampler.set_epoch(epoch)for batch in dataloader:data, target = batck[0].to(rank), batch[1].to(rank) # H2Doptimizer.zero_grad()output = model_fsdp(mode) # 使用fsdp包装的model进行前向传播loss = loss(output, target)loss.backward()optimizer.step()

ZeRO1/2/3

ZeRO 是微软家 DeepSpeed 中的核心技术,思想和 FSDP 是相同,二者都是 通过分片消除模型冗余存储,扩大分布式并行训练能力,只不过 FSDP 是 PyTorch 的官方实现版。

ZeRO(Zero Redundancy Optimizer)有三种策略:

  • ZeRO-1:只分片 优化器状态
  • ZeRO-2:分片 梯度 和 优化器状态 ,对应了 FSDP 的 SHARD_GRAD_OP 策略
  • ZeRO-3:分片 参数、梯度 和 优化器状态,对应了 FSDP 的 FULLY_SHARD 策略

虽然 ZeRO 因为深度集成在 DeepSpeed 中,还可以利用上 DeepSpeed 的其他特性,但从生态偏好上讲,个人更推荐使用 PyTorch官方的 FSDP。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.tpcf.cn/web/84187.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeeCode144二叉树的前序遍历

项目场景: 给你二叉树的根节点 root ,返回它节点值的 前序 遍历。 示例 1: 输入:root [1,null,2,3] 输出:[1,2,3] 解释: 示例 2: 输入:root [1,2,3,4,5,null,8,null,null,6,7…

日本生活:日语语言学校-日语作文-沟通无国界(3)-题目:わたしの友達

日本生活:日语语言学校-日语作文-沟通无国界(3)-题目:わたしの友達 1-前言2-作文原稿3-作文日语和译本(1)日文原文(2)对应中文(3)对应英文 4-老师…

使用 rsync 拉取文件(从远程服务器同步到本地)

最近在做服务器迁移,文件好几个T。。。。只能单向访问,服务器。怎么办!!! 之前一直是使用rsync 服务器和服务器之间的双向同步、备份(这是推的)。现在服务器要迁移,只能单向访问&am…

Linux 并发编程:从线程池到单例模式的深度实践

文章目录 一、普通线程池:高效线程管理的核心方案1. 线程池概念:为什么需要 "线程工厂"?2. 线程池的实现:从 0 到 1 构建基础框架 二、模式封装:跨语言线程库实现1. C 模板化实现:类型安全的泛型…

2013年SEVC SCI2区,自适应变领域搜索算法Adaptive VNS+多目标设施布局,深度解析+性能实测

目录 1.摘要2.自适应局部搜索原理3.自适应变领域搜索算法Adaptive VNS4.结果展示5.参考文献6.代码获取7.算法辅导应用定制读者交流 1.摘要 VNS是一种探索性的局部搜索方法,其基本思想是在局部搜索过程中系统性地更换邻域。传统局部搜索应用于进化算法每一代的解上&…

详细介绍医学影像显示中窗位和窗宽

在医学影像(如DICOM格式的CT图像)中,**窗宽(Window Width, WW)和窗位(Window Level, WL)**是两个核心参数,用于调整图像的显示对比度和亮度,从而优化不同组织的可视化效果…

Unity_VR_如何用键鼠模拟VR输入

文章目录 [TOC] 一、创建项目1.直接创建VR核心模板(简单)2.创建3D核心模板导入XR包 二、添加XR设备模拟器1.打开包管理器2.添加XR设备模拟器3.将XR设备模拟器拖到场景中4.运行即可用键盘模拟VR输入 一、创建项目 1.直接创建VR核心模板(简单&…

SpringBoot定时监控数据库状态

1.application.properties配置文件 # config for mysql spring.datasource.url jdbc\:mysql\://127.0.0.1\:3306/数据库名?characterEncoding\utf8&useSSL\false spring.datasource.username 账号 spring.datasource.password 密码 spring.datasource.validation-quer…

Qt联合Halcon开发一:Qt配置Halcon环境【详细图解流程】

在Qt中使用Halcon库进行图像处理开发,可以有效地结合Qt的图形界面和Halcon强大的计算机视觉功能。下面是详细的配置过程,帮助你在Qt项目中成功集成Halcon库。 步骤 1: 安装Halcon软件并授权 首先,确保你已经在电脑上安装了Halcon软件&#x…

一体化(HIS系统)医院信息系统,让医疗数据互联互通

在医疗信息化浪潮下,HIS系统、LIS系统、PACS系统、电子病历系统等信息系统成为医疗机构必不可少的一部分,从患者挂号到看诊,从各种检查到用药,从院内治疗到院外管理……医疗机构不同部门、不同科室的各类医疗、管理业务几乎都初步…

Spring Boot 的 3 种二级缓存落地方式

在高并发系统设计中,缓存是提升性能的关键策略之一。随着业务的发展,单一的缓存方案往往无法同时兼顾性能、可靠性和一致性等多方面需求。 此时,二级缓存架构应运而生,本文将介绍在Spring Boot中实现二级缓存的三种方案。 一、二…

Android Studio Profiler使用

一:memory 参考文献: AndroidStudio之内层泄漏工具Profiler使用指南_android studio profiler-CSDN博客

Zephyr boot

<!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width, initial-scale1.0"> <title>Zephyr设备初始化机制交互式解析…

腾讯地图Web版解决热力图被轮廓覆盖的问题

前言 你好&#xff0c;我是喵喵侠。 还记得那天傍晚&#xff0c;我正对着电脑调试一个腾讯地图的热力图页面。项目是一个区域人流密度可视化模块&#xff0c;我加了一个淡蓝色的轮廓图层用于表示区域范围&#xff0c;热力图放在下面用于展示人流热度。效果一预览&#xff0c;…

【JVMGC垃圾回收场景总结】

文章目录 CMS在并发标记阶段&#xff0c;已经被标记的对象&#xff0c;又被新生代跨带引用&#xff0c;这时JVM会怎么处理?为什么 Minor GC 会发生 STW&#xff1f;有哪些对象是在栈上分配的&#xff1f;对象在 JVM 中的内存结构为什么需要对齐填充&#xff1f;JVM 对象分配空…

3_STM32开发板使用(STM32F103ZET6)

STM32开发板使用(STM32F103ZET6) 一、概述 当前所用开发板为正点原子精英板,MCU: STM32F103ZET6。一般而言,拿到板子之后先要对板子有基础的认识,包括对开发板上电开机、固件下载、调试方法这三个部分有基本的掌握。 二、系统开机 2.1 硬件连接 直接接电源线或Type-c线…

crackme012

crackme012 名称值软件名称attackiko.exe加壳方式无保护方式serial编译语言Delphi v1.0调试环境win10 64位使用工具x32dbg,PEid破解日期2025-06-18 -发现是 16位windows 程序环境还没搭好先留坑

CppCon 2016 学习:I Just Wanted a Random Integer

你想要一个随机整数&#xff0c;用于模拟随机大小的DNA读取片段&#xff08;reads&#xff09;&#xff0c;希望覆盖不同长度范围&#xff0c;也能测试边界情况。 代码部分是&#xff1a; #include <cstdlib> auto r std::rand() % 100;它生成一个0到99之间的随机整数&…

MySQL层级查询实战:无函数实现部门父路径

本次需要击毙的MySQL函数 函数主要用于获取部门的完整层级路径&#xff0c;方便在应用程序或SQL查询中直接调用&#xff0c;快速获得部门的上下级关系信息。执行该函数之后简单使用SQL可以实现数据库中部门名称查询。例如下面sql select name,GetDepartmentParentNames(du.de…

Python初学者教程:如何从文本中提取IP地址

Python初学者教程:如何从文本中提取IP地址 在网络安全和数据分析领域,经常需要从文本文件中提取IP地址。本文将引导您使用Python创建一个简单但实用的工具,用于从文本文件提取所有IP地址并将其保存到新文件中。即使您是编程新手,也可以跟随本教程学习Python的基础知识! …