Megatron-LM中的权重初始化
现象描述:在某平台上上训练大模型时,受限于内存不够,需要使用模型并行。
例如模型结构:下采样(包含卷积等层)⇒SwinTransformer⇒ 上采样(包含卷积等),Megatron-LM只对Transformer部分进行了初始化。
当tp=6时,如果全部随机初始化权重,transformer层在不同rank上的权重是不同的(因为每个rank保持不同部分transformer权重);但其他无法并行的层(例如卷积) (经打印验证) 在6个节点都保持了相同的初始化权重。
由此对Megatron-LM和PyTorch中权重的初始化方式产生了疑惑。
随机初始化
我们以torch.nn.Conv2d为例子,它的随机初始化定义在基类
torch.nn.modules.conv._ConvNd
torch/nn/modules/conv.py
- 可以Google搜索:caffe2.ai: xxxtorch api 来查找
Caffe2 - Python API: torch.nn.modules.conv._ConvNd Class Reference
nn.Module是所有神经网络单元(neural network modules)的基类,其中负责初始化参数的函数是 reset_parameters(self)
可以看到,随机参数的初始化分为如下两个步骤:
self.weight = Parameter( torch.Tensor(out_channels, in_channels // group, **kernel_size ) )
self.bias = Parameter(torch.Tensor(out_channels))
这里torch.nn.Parameter 继承自 torch.Tensor,使用Parameter创建的张量:
- 会自动设置 requires_grad 为True。
- 会将该变量加入到 模型的named_parameters当中。
当然,如果没有
reset_parameters 对 self.weight 和 self.bias进行重制。
- weight初始化: init.kaiming_uniform_(self.weight, a=math.sqrt(5)),这里使用的是HE初始化(凯明何的初始化)的均匀分布采样,采样的bound:
$$
bound=\sqrt{\frac{6}{(1+a)^2\times fan_{in}}}
$$其中$fan_{in}$ 是输入神经元数量,输入通道 x 卷积大小K^2
- bias初始化:首先根据weight来计算$fan_{in}$和$fan_{out}$,然后用
$$
bound=\sqrt{\frac{1}{fan_{in}}}
$$采用均匀分布进行采样。
这里,bias和weight都在相同的采样区间里,采用均匀分布来采样。
关于torch.nn.init中的采样方法,可以参见blog:
那么我们来关注另一个问题,有了初始化方法,它又是怎么同随机种子相关联的?
首先,megatron的随机种子是在args.seed参数来设置的,默认1234。
但实际设置时,采用了函数_set_random_seed
这里有些细节:
- 流水线并行组各rank上的seed是不同的,那么流水线并行不同rank上 哪怕是未切分的层也是不同的随机参数。 但是这保证了 不同Transformer层的权重是不一样的。
- 首先,默认情况下,不同的流水线阶段 之间的 种子是不一样的,容易理解 不同transformer层的初始化种子不一致。
- 可选的,可以让不同数据对应的模型副本采用不同的随机初始化方法。
- 设置了三种种子:
- random.seed:影响random.random… random.randint等
- np.random.seed:影响np.random.rand()、np.random.randn()等
- torch.manual_seed:影响torch.rand()、torch.randn()和torch.randint()等。
这意味着:
张量并行组的各rank上的seed是相同的,也就意味着 随机发生器所生成的随机序列是确定的,对于非切分的层,对应层的初始化参数是完全相同的。
- 但是不同次序组织的层,哪怕相同规模和设置,由于随机生成的发生次序不一样,因此彼此之间随机参数不同。
- 而不同规模或者类型的层,哪怕两次运行位于相同的随机次序上,由于采样的范围、采样的方法不同,生成的随机参数也是不一样的。
- 实验证明,torch.init..随机方法,每次会消耗一个随机生成器产生的随机状态,比如在一个卷积层中,初始化了两个参数,消耗了两个随机状态, 下一个卷积或者层的初始化参数会使用第三个状态。 那么,我们可以用两个torch.init.去替代第一个卷积,保证下一个卷积生成的随机参数还是相同的。
(我们可以尝试,在随机生成参数时,让并行卷积层在相同的随机生成次序上,在相同采样范围和采样方法? )
那么对于张量并行,位于不同rank上的切分的层,是如何实现权重不同的?
我们可以在megatron-sw/megatron/core/tensor_parallel/layers.py
当中ColumnParallelLinear或者 RowParallelLinear找到答案:
对于weight而言,生成的是一个列被切分后大小,cpu初始化的话,这里还有一个master_weight
这里可以看到,创建了一个master_weight权重副本,然后对它进行了切分,根据rank来返回不同的weight_list切片weight_list[rank::world_size]
,这里的start=rank,step=world_size. 貌似是比较安全的写法?但如果 切分份数就是world_size的情况下,显然是取 了rank对应的权重切片。
然后再将本rank的切片weight重新用cat的方式组织,覆盖写回weight当中。
但是在gpu版本的初始化中,是直接对weight进行了初始化。
这里保证了每个gpu(哪怕在随机种子相同的情况下)单独创建一个互不相同的独立RNG状态,保证彼此之间生成的是不一样的。
总结下,张量并行初始化线性层参数的过程:
GPU上,通过fork一个rng状态, 让不同GPU分别产生不同的随机参数;
CPU上,通过大家同时生成一个大的相同的随机参数副本,然后根据张量并行rank来选择不同的切片作为自己的随机参数。
张量并行中,(非切分)层的随机初始化过程是完全一致的,因为不同节点的种子一样,随机初始化的次序也相同。
但是流水线并行,会在megatron设置随机种子时,让不同流水线阶段rank的随机种子不同。