虽然 GPU 对深度学习计算有普遍明显的加速作用,但其显存也是有限的(如 V100 的最大显存值也仅有 32G),而深度学习模型的训练和推理往往需要大量的显存,用来支持更大的模型和更大的 batch size。如何更高效地利用 GPU 显存,在一张卡或一台机器上同时承载更多的训练和预测任务,让有限的显存支持多个开发者同时进行实验,执行各自的任务呢?

穷!深度学习中如何更好地利用显存资源?

飞桨 v1.7 在 GPU 显存使用策略方面做了如下 3 点升级:

  1. 默认使用显存自增长 AutoGrowth 策略,根据模型实际占用的显存大小,按需自动分配显存,并且不影响训练速度。在模型实际占用显存不多的情况下,同一张 GPU 卡可以同时运行多个深度学习训练 / 预测任务。

  2. 支持限制任务的最大显存策略,每个任务只能在限定的显存量下运行,实现同一张 GPU 卡多个任务间的显存隔离

  3. 默认使用 Lazy 显存分配方式。只有 GPU 卡工作时才自动分配显存,实现不同 GPU 卡上的任务的相互隔离,可以在一台机器上实现更灵活的任务排布。

这三种显存策略在飞桨是如何实现的?下面我们走进飞将框架一探究竟。
01
AutoGrowth 实现显存按需分配,且不影响训练速度
1.7 版本之前,飞桨默认是显存预分配策略(缺省比例是可用显存的 92%),该策略在实现上是比较高效的,但是预分配比例的设置是一个比较头疼的事情。如果采用 92% 的缺省配置,可以保证大部分情况下任务成功分配,但麻烦的是启动任务之后,即使模型实际占用显存较小,也无法再启动其他的任务了。因此飞桨 v1.7 升级为显存自增长按需分配的 AutoGrowth 作为默认的显存分配策略
考虑对模型训练速度的影响,如果直接使用 cudaMalloc 和 cudaFree 接口进行显存分配和释放,调用的过程非常耗时,会严重影响模型的训练和预测速度。飞桨 v1.7 对此进行了改进,AutoGrowth 显存分配策略通过缓存显存的方式提升显存分配和释放速度,如图 1 所示。

穷!深度学习中如何更好地利用显存资源?

图 1 AutoGrowth 显存分配策略示意图
框架内部缓存 Memory Cache 中缓存了若干显存块 block,当用户申请 request_size 大小的显存时,AutoGrowth 策略内部的具体流程如下:

  • 若 Memory Cache 为空,或者所有 block 均小于 request_size,通过调用 cudaMalloc 从 GPU 中申请显存(对应图 1 中的 large_request_size)。
  • 若 Memory Cache 不为空,且存在大于等或者 request_size 大小的 block,查找到满足条件的最小 block,从中分割出 request_size 大小的显存返回给用户。
  • 显存释放时,释放的显存将存储到 Memory Cache 中,不再返回给 GPU。

可以看到,整个策略设计是非常高效的。通过 AutoGrowth 策略,实现按需分配显存的同时,也保证了模型训练速度不受影响。 实验观察
下面以 ResNet50 batch_size=32 单卡训练为例,观察一下,执行 AutoGrowth 策略后,显存的占用情况。
首先您需要在本地安装飞桨 1.7,然后在飞桨 GitHub 中下载模型库代码,运行如下命令,进入”models/PaddleCV/image_classification”目录。

    git clone -b release/1.7 https://github.com/PaddlePaddle/models.gitcd models/PaddleCV/image_classification

执行如下命令启动 ResNet50 单卡训练任务。

    export CUDA_VISIBLE_DEVICES=0python train.py  --model=ResNet50  --data_dir=./data/ILSVRC2012/  --batch_size=32
  • data_dir:设置 ImageNet 数据集的路径。
  • batch_size:设置 batch_size 为 32。

训练任务启动后,运行 nvidia-smi 命令,观察 GPU 显存的占用情况。
运行 1 个 ResNet50 训练任务,显存占用约 4G。(飞桨 1.7 之前,运行 1 个 ResNet50 训练任务,显存空间就完全被占满。)

穷!深度学习中如何更好地利用显存资源?

运行 2 个训练任务,显存占用约 8G。

穷!深度学习中如何更好地利用显存资源?

运行 3 个训练任务,显存占用约 12G。

穷!深度学习中如何更好地利用显存资源?

运行 4 个训练任务,显存占用约 16G,此时显存完全被占满。

穷!深度学习中如何更好地利用显存资源?

实验证明:使用 AutoGrowth 策略后,一张 16G V100 的 GPU 可以并行 4 个 ResNet50 训练任务。那么,AutoGrowth 策略的使用会不会影响模型的训练速度呢?
在模型训练初期,由于 Memory Cache 为空或 block 数量很少,框架会先从 GPU 中申请显存。显存释放时会存储在 Memory Cache 中,因此 Memory Cache 中的 block 会不断增加。后续显存请求会越来越多的从 Memory Cache 中先获取到,因此使用 AutoGrowth 策略后,训练速度保持不变。

穷!深度学习中如何更好地利用显存资源?

02
支持限制任务的最大显存策略,实现单卡多任务间的资源隔离
实际应用中,常会遇到多个开发者使用同一个 GPU 卡进行模型训练的场景,此时需要将 GPU 卡的显存分为若干份,分给开发者独立使用。飞桨 1.7 支持自定义每个任务使用的最大显存策略,用户只需要配置几个参数,即可实现同一张 GPU 卡多个任务间的资源隔离。
例如,若想限定某个任务的最大显存占用量不超过 2048MB,代码如下:

    export  FLAGS_gpu_memory_limit_mb=2048

环境变量 FLAGS_gpu_memory_limit_mb 表示限定每个任务的最大显存占用量,为 uint64 类型的整数,单位为 MB。

  • 默认值为 0,表示飞桨任务可以使用所有可用的显存资源,不设上限。
  • FLAGS_gpu_memory_limit_mb > 0,表示飞桨任务仅可使用不超过 FLAGS_gpu_memory_limit_mb MB 的显存。

若 FLAGS_gpu_memory_limit_mb > 0,飞桨框架内部会对 GPU 显存分配 cudaMalloc 和释放 cudaFree 这两个接口进行监控,保证任务占用的显存量不超过用户设定的阈值。 举例来说,用户通过下述代码申请了 2G 大小的 Numpy 数组,并拷贝到飞桨的 GPU Tensor 中。

          *
    import paddle.fluid as fluidimport numpy as np  
    # 申请 2G 大小的 Numpy 数组 two_gb_numpy_array = np.ndarray([2, 1024, 1024, 1024], dtype='uint8')  
    place = fluid.CUDAPlace(0)t = fluid.Tensor()t.set(two_gb_numpy_array, place) # 将 2G 大小的 Numpy 数组拷贝到 GPU 上
  • 若未设置 FLAGS_gpu_memory_limit_mb,上述飞桨任务可正常运行,任务占用 2048MB 显存。
  • 若设置了 FLAGS_gpu_memory_limit_mb=1024,则会报出显存不足错误,如图 2 所示。表明任务使用的最大显存量被限定为 1024MB。

穷!深度学习中如何更好地利用显存资源?

图 2 显存不足报错提示 03
默认 LAZY 显存分配方式,实现不同卡上训练任务的隔离
下面通过执行一段简单的飞桨训练代码,了解下使用 LAZY 策略后,显存分配方式的变化。假设用户有 2 张 GPU 卡,分别为 GPU 0 和 GPU 1。GPU 1 被训练任务占用了 16092MB 显存,几乎将显存完全占满。

穷!深度学习中如何更好地利用显存资源?

在 LAZY 模式下,用户仍可以在 GPU 0 上执行训练任务,如下代码所示。

                            *
    import paddle.fluid as fluidimport numpy as np  
    x = fluid.data(name='x', shape=[None, 784], dtype='float32')fc = fluid.layers.fc(x, size=10)loss = fluid.layers.reduce_mean(fc) sgd = fluid.optimizer.SGD(learning_rate=1e-3)sgd.minimize(loss)  
    place = fluid.CUDAPlace(0)   # 使用 GPU 0 卡进行训练 exe = fluid.Executor(place)exe.run(fluid.default_startup_program())  
    BATCH_SIZE = 32BATCH_NUM = 1000000  
    for batch_id in range(BATCH_NUM):    x_np = np.random.random([BATCH_SIZE, 784]).astype('float32')    loss_np, = exe.run(fluid.default_main_program(),     feed={x.name: x_np}, fetch_list=[loss])    print('Batch id {}, loss {}'.format(batch_id, loss_np))

运行 nvidia-smi 命令,观察 GPU 0 和 GPU 1 上的显存占用情况。

穷!深度学习中如何更好地利用显存资源?

GPU 0 占用了 750MB 的显存,但 GPU 1 卡上的显存占用量仍为 16092MB,与任务启动前的显存占用量一致,说明在 GPU 0 上执行训练任务在 GPU 1 上不占用任何显存,实现了不同卡上训练任务的隔离。
以上就是飞桨 1.7 给开发者们带来的 3 个全新的显存分配策略,希望能帮助大家更高效的完成模型训练和预测。推荐阅读

Django 采用新的项目治理模型

Apache 软件基金会是如何运作的

苹果招兵买马,或在开源领域有大动作?

GitHub 被“中介”攻击了?中间人攻击?

Linus 谈居家办公:不要在家中重新搞一个办公室

来源链接:mp.weixin.qq.com