优化模型训练时间-Transformer

周末在家发现了一个挺有意思的项目:

优化模型训练时间-Transformer-图片1

「也就是将你模型中的参数全部存储为一个连续的内存块,加速你的模型训练。」

于是我抱着试试看的心态,基于Fairseq和LightSeq分别实现了两个单层的Transformer编码层模型,简单写了一个例子试了一下。

安装

首先为了运行我这个例子,你需要安装上面提到的contiguous-params库。然后还需要安装fairseqlightseq库。

  1. pip install contiguous-params fairseq lightseq

一个简单的例子

我这里创建了一个模型,就是单层的Transformer编码层,然后随机输入一个向量,损失函数就是输出向量的所有元素的平方均值。

然后测试了采用参数连续化前后,前向传播、反向传播、梯度更新三部分的时间消耗。

  1. import time
  2. from dataclasses import dataclass
  3. import copy
  4.  
  5. import torch
  6. from fairseq.modules.transformer_layer import TransformerEncoderLayer
  7. from lightseq.training.ops.pytorch.transformer_encoder_layer import LSTransformerEncoderLayer
  8. from contiguous_params import ContiguousParams
  9.  
  10. def get_time():
  11. '''CUDA同步并获取当前时间'''
  12.  
  13. torch.cuda.synchronize(device="cuda:0")
  14. return time.time()
  15.  
  16. def ls_config_to_fs_args(config):
  17. '''将LightSeq的config转换为Fairseq的args'''
  18.  
  19. @dataclass
  20. class Args:
  21. encoder_embed_dim: int
  22. encoder_ffn_embed_dim: int
  23. encoder_attention_heads: int
  24. dropout: float
  25. attention_dropout: float
  26. activation_dropout: float
  27. encoder_normalize_before: bool
  28.  
  29. args = Args(
  30. config.hidden_size,
  31. config.intermediate_size,
  32. config.nhead,
  33. config.hidden_dropout_ratio,
  34. config.attn_prob_dropout_ratio,
  35. config.activation_dropout_ratio,
  36. config.pre_layer_norm
  37. )
  38. return args
  39.  
  40. def train(model, inputs, masks, contiguous=False):
  41. '''训练过程'''
  42.  
  43. model.to(device="cuda:0")
  44. model.train()
  45. if contiguous:
  46. parameters = ContiguousParams(model.parameters())
  47. opt = torch.optim.Adam(parameters.contiguous(), lr=1e-3)
  48. else:
  49. opt = torch.optim.Adam(model.parameters(), lr=1e-3)
  50.  
  51. fw_time, bw_time, step_time = 0, 0, 0
  52.  
  53. for epoch in range(1000):
  54. opt.zero_grad()
  55.  
  56. start_time = get_time()
  57. outputs = model(inputs, masks)
  58. loss = torch.square(outputs).mean()
  59. fw_time = get_time() - start_time
  60.  
  61. start_time = get_time()
  62. loss.backward()
  63. bw_time = get_time() - start_time
  64.  
  65. start_time = get_time()
  66. opt.step()
  67. step_time = get_time() - start_time
  68.  
  69. if epoch % 200 == 0:
  70. print("epoch {:>3d}: loss = {:>5.3f}".format(epoch, loss))
  71.  
  72. return fw_time, bw_time, step_time
  73.  
  74. if __name__ == "__main__":
  75. # 定义LightSeq的config
  76. config = LSTransformerEncoderLayer.get_config(
  77. max_batch_tokens=4096,
  78. max_seq_len=256,
  79. hidden_size=128,
  80. intermediate_size=512,
  81. nhead=16,
  82. attn_prob_dropout_ratio=0.1,
  83. activation_dropout_ratio=0.1,
  84. hidden_dropout_ratio=0.1,
  85. pre_layer_norm=True,
  86. fp16=False,
  87. local_rank=0
  88. )
  89. # 将LightSeq的config转换为Fairseq的args
  90. args = ls_config_to_fs_args(config)
  91.  
  92. # 随机生成输入
  93. bsz, sl = 50, 80
  94. inputs = torch.randn(bsz, sl, config.hidden_size).to(device="cuda:0")
  95. masks = torch.zeros(bsz, sl).to(device="cuda:0")
  96.  
  97. # 定义LightSeq模型并训练
  98. ls_model = LSTransformerEncoderLayer(config)
  99. ls_fw_time, ls_bw_time, ls_step_time = train(ls_model, inputs, masks)
  100. # 定义连续化参数的LightSeq模型并训练
  101. config_cont = copy.deepcopy(config)
  102. ls_model_cont = LSTransformerEncoderLayer(config_cont)
  103. ls_c_fw_time, ls_c_bw_time, ls_c_step_time = train(ls_model_cont, inputs, masks, contiguous=True)
  104.  
  105. inputs = inputs.transpose(0, 1)
  106. masks = masks > 0.5
  107. # 定义Fairseq模型并训练
  108. fs_model = TransformerEncoderLayer(args)
  109. fs_fw_time, fs_bw_time, fs_step_time = train(fs_model, inputs, masks)
  110. # 定义连续化参数的Fairseq模型并训练
  111. fs_model_cont = TransformerEncoderLayer(args)
  112. fs_c_fw_time, fs_c_bw_time, fs_c_step_time = train(fs_model_cont, inputs, masks, contiguous=True)
  113.  
  114. print("LightSeq time: {:.3f}s, {:.3f}s, {:.3f}s".format(ls_fw_time, ls_bw_time, ls_step_time))
  115. print("LightSeq (cont) time: {:.3f}s, {:.3f}s, {:.3f}s".format(ls_c_fw_time, ls_c_bw_time, ls_c_step_time))
  116. print("Fairseq time: {:.3f}s, {:.3f}s, {:.3f}s".format(fs_fw_time, fs_bw_time, fs_step_time))
  117. print("Fairseq (cont) time: {:.3f}s, {:.3f}s, {:.3f}s".format(fs_c_fw_time, fs_c_bw_time, fs_c_step_time))

详细讲解

这里最主要的地方就两行:

  1. parameters = ContiguousParams(model.parameters())
  2. opt = torch.optim.Adam(parameters.contiguous(), lr=1e-3)

首先用ContiguousParams类封装model.parameters(),然后将封装后的parameters.contiguous()送进优化器中,这里送进去的就已经是连续存储的一整块参数了。

我们详细阅读ContiguousParams的源码,可以发现实现很简单:

https://github.com/PhilJd/contiguous_pytorch_params/blob/master/contiguous_params/params.py

核心代码就是下面这个函数,注释中我都详细解释了每一步在干嘛:

  1. def make_params_contiguous(self):
  2. index = 0
  3. # 遍历所有的参数
  4. for p in self._parameters:
  5. # 计算参数p的大小
  6. size = p.numel()
  7. # 在连续参数块中的对应位置赋值参数p
  8. self._param_buffer[index:index size] = p.data.view(-1)
  9. # 将参数p的数值和梯度都重新指向连续参数块和连续梯度块的对应位置
  10. p.data = self._param_buffer[index:index size].view(p.data.shape)
  11. p.grad = self._grad_buffer[index:index size].view(p.data.shape)
  12. # 连续内存块位置偏移到下一个参数
  13. index = size
  14. # 连续参数块的梯度设置为连续梯度块
  15. self._param_buffer.grad = self._grad_buffer

所以在封装了原始参数之后,之后模型计算就会从连续内存块中对应位置取出数值,然后进行计算。

运行结果

我在V100显卡上运行了一下上面的例子,结果如下:

优化模型训练时间-Transformer-图片2

可以看出,LightSeq在采用参数连续化前后,三部分运行时间几乎没有任何变化,这主要是由于LightSeq已经在模型内部做过参数连续化了,因此速度已经很快了。

而Fairseq前后的第三部分,也就是参数更新部分时间缩减非常多,从1.5秒缩短到了0.1秒,总的训练时间几乎缩短了将近一半。

最后对比LightSeq和Fairseq可以明显发现,LightSeq的训练时间比Fairseq快非常多。主要是因为LightSeq采用了算子融合等各种技术,加速了Transformer模型的训练。

总结

所以在你的 「任意」 PyTorch模型中,都可以用上面的参数连续化技术大大加快训练速度。

而如果你的模型是Transformer类模型,那还可以直接用字节跳动开源的LightSeq训练加速引擎,更加方便。

如果你是TensorFlow爱好者,还可以直接用字节跳动开源的NeurST序列生成库进行训练,里面直接集成了LightSeq,所以训练很快。

地址

参数连续化

https://github.com/PhilJd/contiguous_pytorch_params

LightSeq

https://github.com/bytedance/lightseq

NeurST

https://github.com/bytedance/neurst

发表评论

匿名网友

拖动滑块以完成验证
加载失败