解决了PyTorch 使用torch.nn.DataParallel 进行多GPU训练的一个BUG:模型(参数)和数据不在相同设备上

解决了PyTorch 使用torch.nn.DataParallel 进行多GPU训练的一个BUG:模型(参数)和数据不在相同设备上

博客地址:https://senyang-ml.github.io/2019/07/20/pytorch-multigpu/

解决了PyTorch 使用torch.nn.DataParallel 进行多GPU训练的一个BUG:

模型(参数)和数据不在相同设备上

使用torch.nn.DataParallel进行多GPU训练时出现了一个BUG, 困扰许久:

RuntimeError: Expected tensor for argument #1 'input' to have the same device as tensor for argument #2 'weight'; but device 1 does not equal 0 (while checking arguments for cudnn_convolution)

这个错误表明, input数据在device 1 上, 而模型的参数在device 0 上 (暗示数据是被拆分到了各个GPU上,但是BUG出现位置的此处参数可能没有成功复制到其他GPU上, 或者说, 还是调用了复制前的那个参数地址)

之前调试了好久, 也没有解决掉, 在Github上有一个issue和我的问题很像:
https://github.com/pytorch/pytorch/issues/8637
但是我还是没有找到自己的问题在哪里.

今天, 我又准备再此挑战这个难题

经过6个小时的print调试法以及后面关键的VScode的Debug功能, 我大功告成,找到了问题所在!!!

在我的A(nn.Module)类的forward 前向计算函数里面, 有一处调用了一个该类的列表self.cell_fabrics属性, 其中,列表中的元素是直接通过

self.cell_fabrics = [self.cell_1,...,self.cell_n]

这个语句来赋值的, 其中每个self.cell也是nn.Module类。

当我们使用torch.nn.DataParallel复制了A(nn.Module)类的模型放到设备cuda:1上后(注意:cuda:0上始终保存着原始的模型),并且当多GPU并行处理的程序,运行到下面代码段时:

def forward(self,x)
    for layer in self.cell_fabrics:
        for cell in layer:
           y = cell (x,)

debug输出显示:x是在设备cuda:1 上面, 而 cell 中的参数明显都在 cuda:0上。

也就是说:

此时,前传调用的self.cell_fabrics 列表中的各个元素 (self.cell)的地址,还是指向在没有进行torch.nn.DataParallel之前的nn.module 的那些self.cell, 而nn.DataParallel类的nn.module的参数都默认存放在device(type='cuda',index=0)上 .

torch.nn.Module的广播机制和列表赋值方式

torch.nn.DataParallel(model,device_ids=[range(len(gpus))])的机制是, 将属于nn.module类的model以及其广播的所有nn.module子类的上的所有参数,复制成len(gpus)份,送到各个GPU上. 这种传播必须通过其子类通过注册(register)成为nn.module的属性才得以完成, 其属性为列表中的元素是不会被复制的, 所以其属性中的元素还是存放在默认设备device 0

所以 在使用torch.nn.DataParallel进行多GPU训练的时候, 请注意:所有属于模型参数的模块以及其子模块必须以nn.Module的类型注册为模型的属性, 如果需要一个列表来批量存放子模块或者参数时, 请采用nn.ModuleList或者nn.ModuleDict这样的继承了nn.Module的类来进行定义, 并且在forward(self,)前向传播的过程中,需要直接调用属于 nn.Module,nn.ModuleList或者nn.ModuleDict 这样的属性。

那么torch.nn.DataParallel将会正常地将模型参数准确复制到多个GPU上, 并默认根据数据的batchsize的大小平分成GPU的数量分别送到相应的GPU设备上,

运用多线程的方式, 同时对这些数据进行加工处理, 然后收集各个GPU上最终产生对模型的参数的梯度, 汇总到一起更新原模型的参数!

参考:

  1. https://github.com/pytorch/pytorch/issues/8637
  2. https://pytorch.org/docs/stable/nn.html#dataparallel-layers-multi-gpu-distributed
<think>好的,我现在遇到了一个错误:在使用PyTorch的FSDP(完全分片数据并行)时,出现了ValueError,提示“Must flatten tensors with uniform dtype but got torch.float16 and torch.uint8”。我需要解决这个问题。让我仔细想想应该怎么处理。 首先,我得理解这个错误的原因。错误信息说在flatten张量时需要统一的dtype,但实际得到了float16uint8两种同的数据类型。这说明在FSDP处理模型参数或缓冲区时,存在数据类型的张量被混合在一起了,而FSDP要求同一组中被分片的张量必须是相同数据类型。 那FSDP的工作原理是怎样的呢?FSDP会将模型参数分片到各个GPU上,同时在计算时按需获取其他分片。为了高效处理,它会将同一层或同一组的参数合并成一个大的扁平张量。这时候如果这些参数数据类型一致,就会导致无法合并,从而报错。 接下来,我需要检查模型中的参数缓冲区是否有同的数据类型。可能的场景包括: 1. 某些层使用同的数据类型。比如大部分参数是float16,但某一层用了uint8。 2. 缓冲区(buffer)可能被注册为uint8类型。例如,某些自定义模块中的缓冲区可能存储了uint8的数据,比如量化相关的参数或中间结果。 3. 可能在使用混合精度训练时,某些参数未被正确转换类型,或者存在未跟随自动转换的缓冲区。 如何具体排查呢? 首先,检查模型中所有参数缓冲区的数据类型。可以通过遍历模型的parameters()buffers()来查看每个参数的dtype。例如: for name, param in model.named_parameters(): print(f"Parameter: {name}, dtype: {param.dtype}") for name, buffer in model.named_buffers(): print(f"Buffer: {name}, dtype: {buffer.dtype}") 找到那些是float16的,尤其是uint8类型的参数或缓冲区。如果发现了uint8的缓冲区,可能需要将其类型转换为float16或者其他兼容的类型。 其次,考虑FSDP的包装方式。FSDP在包装模块时,会递归地处理子模块。如果某个子模块的参数或缓冲区与其他子模块的数据类型同,而它们被分在同一个FSDP单元中,就会导致问题。这时,可能需要调整FSDP的包装策略,将数据类型的层分到同的FSDP单元中。例如,使用auto_wrap_policy参数,或者手动包装这些层,确保每个FSDP实例内的参数缓冲区数据类型一致。 例如,假设模型中有一个使用uint8缓冲区的层,可以将该层单独包装为一个FSDP模块,避免与其他层合并。或者,如果可能的话,将该缓冲区的数据类型改为float16,但需要注意这样做是否会影响模型的功能,比如如果是存储量化后的整数类型的数据,可能能随意更改类型。 另外,混合精度训练时,如果使用了FSDP的参数分片,需要注意是否正确处理了参数类型。例如,在设置混合精度时,是否所有参数都被正确转换为目标类型,或者是否有某些参数未被覆盖到。 可能的解决方案步骤: 1. 检查并修改模型中所有参数缓冲区的数据类型,确保在同一个FSDP单元内的参数缓冲区都是同一种类型。比如,将uint8的缓冲区转换为float16。 2. 调整FSDP的包装策略,把数据类型的层分离开,单独用FSDP包装,避免混合在一起。 3. 如果某个层的缓冲区必须是uint8类型,可能需要禁用FSDP对该层的分片处理,或者寻找其他变通方法,例如将该缓冲区排除在FSDP的分片之外,但这可能需要深入理解FSDP的内部机制,或者查阅相关文档社区讨论。 举个例子,假设模型中有个自定义层,里面有一个uint8的缓冲区。这个缓冲区可能是用来存储某种二值化的权重或类似的量化信息。如果将其数据类型改为float16会影响模型的计算,那么可能需要将该层单独包装,或者在FSDP配置中排除这个缓冲区,或者调整FSDP的分片逻辑。 另外,可能需要检查PyTorchFSDP的版本,确认是否有已知的bug或者是否在最新版本中已修复。有时候升级版本可以解决问题。 总结,解决步骤大致如下: 1. 确定问题根源:找到模型中存在的数据类型的参数或缓冲区。 2. 修改数据类型或调整FSDP包装方式,确保同一FSDP单元内数据类型一致。 3. 重新运行代码,验证问题是否解决。 如果在检查过程中发现某个缓冲区的类型确实是uint8,并且需要保留,那么可能需要将该模块单独包装,或者调整其类型。例如,如果这个缓冲区是用于存储二值化的mask,可能可以转换为bool类型,或者转换为float类型。但需要确认模型逻辑是否允许这样的修改。 另一个可能的场景是,用户可能在数据预处理或模型中的某个操作中意外创建了uint8类型的张量,并将其作为缓冲区注册到模型中。这时候需要检查自定义模块的初始化过程,确保所有的缓冲区都是正确的类型。 例如,假设有一个模块在初始化时创建了一个缓冲区: class MyModule(nn.Module): def __init__(self): super().__init__() self.register_buffer('my_buffer', torch.tensor([1, 2, 3], dtype=torch.uint8)) 这个my_buffer会被注册为uint8类型,导致FSDP处理时出现问题。解决方法是将这个缓冲区的类型改为兼容的类型,比如torch.int64或者torch.float16,视情况而定: self.register_buffer('my_buffer', torch.tensor([1, 2, 3], dtype=torch.int64)) 或者,如果这个缓冲区需要在训练使用浮点数类型: self.register_buffer('my_buffer', torch.tensor([1., 2., 3.], dtype=torch.float16)) 当然,这需要根据具体的模块功能来决定是否可行。 还有一种情况是,当使用混合精度训练时,可能模型的一部分被转换为float16,而另一部分没有被正确转换,例如自定义层中的参数没有被自动处理。这时候需要手动确保所有参数缓冲区都被正确设置类型,或者在模型初始化时就统一设置。 总的来说,解决这个问题的关键在于确保在同一个FSDP实例中的所有张量(参数缓冲区)都具有相同数据类型,通过检查调整模型结构或FSDP的包装策略来实现这一点。在使用</think> PyTorch 的 **FSDP(Fully Sharded Data Parallel)** 时,遇到 `ValueError: Must flatten tensors with uniform dtype but got torch.float16 and torch.uint8` 错误,通常是因为 FSDP 在合并参数时发现数据类型的张量(如 `float16` `uint8`)。以下是解决步骤: --- ### **1. 定位问题根源** FSDP 要求同一分片组内的参数缓冲区数据类型一致。错误提示表明模型中存在 `torch.uint8` 类型的张量(可能是参数或缓冲区),而其他张量是 `torch.float16`。 #### **排查方法:** ```python # 检查所有参数缓冲区的数据类型 for name, param in model.named_parameters(): print(f"参数: {name}, dtype: {param.dtype}") for name, buf in model.named_buffers(): print(f"缓冲区: {name}, dtype: {buf.dtype}") ``` --- ### **2. 修改数据类型或调整模型结构** #### **情况一:存在 `uint8` 缓冲区** 如果某个缓冲区(Buffer)是 `uint8` 类型(例如存储二值化权重或掩码): - **方法 1:修改缓冲区类型** 将 `uint8` 改为 `float16` 或 `bool`(如允许): ```python # 示例:在自定义模块中将缓冲区初始化为 float16 class MyModule(nn.Module): def __init__(self): super().__init__() self.register_buffer("my_buffer", torch.tensor([1.0], dtype=torch.float16)) ``` - **方法 2:排除该缓冲区** 如果必须保留 `uint8`,将该模块单独包装为独立的 FSDP 单元,避免与其他层混合: ```python from torch.distributed.fsdp import FullyShardedDataParallel as FSDP # 假设 problematic_layer 是包含 uint8 缓冲区的模块 model.problematic_layer = FSDP(model.problematic_layer) model = FSDP(model) # 包装其余部分 ``` #### **情况二:参数类型匹配** 如果某层参数为 `uint8`(罕见,通常参数为 `float32/float16`): - 检查是否误将参数设置为 `uint8`,并修正为 `float16`: ```python # 手动修改参数类型 with torch.no_grad(): param.data = param.data.to(torch.float16) ``` --- ### **3. 调整 FSDP 包装策略** 通过 **`auto_wrap_policy`** 或手动包装,确保数据类型的模块分属同的 FSDP 单元: ```python from torch.distributed.fsdp import FSDP, AutoWrapPolicy # 自定义包装策略(示例:按模块类型拆分) auto_wrap_policy = AutoWrapPolicy({ nn.TransformerEncoderLayer, nn.TransformerDecoderLayer, }) model = FSDP( model, auto_wrap_policy=auto_wrap_policy, mixed_precision=True, # 若使用混合精度 ) ``` --- ### **4. 检查混合精度设置** 若启用混合精度,确保所有参数梯度类型兼容: ```python from torch.distributed.fsdp import MixedPrecision policy = MixedPrecision( param_dtype=torch.float16, reduce_dtype=torch.float16, buffer_dtype=torch.float16, ) model = FSDP(model, mixed_precision=policy) ``` --- ### **5. 验证修改后模型** 重新运行代码,确认错误是否解决。若仍存在问题,再次检查是否有遗漏的 `uint8` 张量。 --- ### **总结** - **关键点:** 确保同一 FSDP 单元内的参数缓冲区数据类型一致。 - **常见场景:** 自定义模块中的 `uint8` 缓冲区未正确处理。 - **终极方案:** 调整数据类型或通过包装策略隔离数据类型的模块。
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值