51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

显存使用分析(PyTorch)

我们一直使用 PyTorch 进行模型训练,有时会出现显存不足的情况。除了找到对应的解决办法,比如:累加梯度、使用自动混合精度,还应该了解训练时,显存究竟在哪些环节被大量占用。主要有以下四个环节:

  1. CUDA 运行内存

  2. 模型的固定参数

  3. 模型的前向计算

  4. 模型的反向计算

  5. 优化方法统计量

  6. CUDA 运行内存 {#title-0} =======================

CUDA(Compute Unified Device Architecture,,计算统一设备架构),是显卡厂商 NVIDIA 推出的运算平台。通过它我们就利用 GPU 的处理能力,大幅提升计算性能。

CUDA 对我们来说,本质是一套在 GPU 硬件设备上运行的软件程序,我们的计算任务需要在该软件平台基础上运行才能利用到 GPU 的运算能力。既然是软件程序,所以 CUDA 运行起来时也会占用一部分的显存,至于占用多大,这得看 CUDA 的版本,有的占 600M 左右,有的会占到 1G 以上。

首先,我们先了解下 PyTorch 的内存使用机制。GPU 显存相当于我们全部可用的资源,掌握 C/C++ 的同学会知道,频繁的资源申请和释放操作,比如 C 的 malloc/free ,C++ 的 new/delete 会非常降低系统的性能。为了减少此类的操作,就有了资源池的概念。其思想是:预先从去全部可用资源中申请较大一块资源,当用户程序需要资源时,从资源池中申请,这就跳过了复杂的、耗时的系统调用过程,资源回收时,将资源放到资源池中。当资源池用尽时,再从可用资源中申请。这样提高了程序在资源使用这个环节的效率。

PyTorch 为张量分配内存资源也是使用这种方法,先申请较大的内存,张量需要需要内存时从内存池获取,不用时,归还到内存池。所以,如果 PyTorch 不使用这种资源缓存的机制,那么运行效率将会非常慢。

我们接下来,通过一段代码来验证下,CUDA 软件平台运行时,会占用部分显存,先安装一个库:

pip install pynvml
import torch
import pynvml


# 初始化 pynvml 库
pynvml.nvmlInit()
convert = lambda x: int(x / 1024 / 1024)
# 获得显卡设备对象
device_object = pynvml.nvmlDeviceGetHandleByIndex(0)


# 查看显存资源
def show_usage():
    # 获得显存信息
    device_memory = pynvml.nvmlDeviceGetMemoryInfo(device_object)
    # 全部可用显存
    total = convert(device_memory.total)
    # 已经使用显存
    used = convert(device_memory.used)
    # 剩余可用显存
    free = convert(device_memory.free)
    print('总共:', total, '使用:', used, '剩余:', free)


# 1. CUDA 初始化会占用部分显存
def test01():
    show_usage()
    # 如果张量创建在 CPU 是不会占用显存,并且也不会初始化 CUDA
    torch.tensor(0.0, device='cpu')
    show_usage()
    torch.tensor(0.0, device='cuda')
    # 清空缓存
    torch.cuda.empty_cache()
    show_usage()

if __name__ == '__main__':
    test01()

程序输出结果:

总共: 5932 使用: 0 剩余: 5932
总共: 5932 使用: 0 剩余: 5932
总共: 5932 使用: 586 剩余: 5346

上面代码如果不清空缓存,输出结果 588,而不是 586。588 = 586 + PyTorch 缓存。另外,我们创建的 cuda 张量并没有建立引用,所以创建之后会被自动回收,此时清理缓存才是 586,否则的话仍然是 588. 这是因为每次向 cuda 设备创建张量,都会分配 512 的倍数的显存。

import torch

def test02():

    # 0
    print(torch.cuda.memory_allocated())
    a = torch.tensor(0.0, device='cuda')
    # 512
    print(torch.cuda.memory_allocated())
    # 1024
    b = torch.tensor(0.0, device='cuda')
    print(torch.cuda.memory_allocated())
    # 1536
    c = torch.tensor(0.0, device='cuda')
    print(torch.cuda.memory_allocated())



if __name__ == '__main__':
    test02()

程序输出结果:

0
512
1024
1536

torch.cuda.memory_allocated 可以获得目前分配的内存数量。

  1. 模型的固定参数 {#title-1} =====================

这一部分也是比较容易理解的,加载模型就是加载模型参数。所以,模型的参数会占用一部分的显存。默认情况下, PyTorch 中的参数使用的是 float32 类型。请看下面的代码:

import torch
import torch.nn as nn


def test01():
    print(torch.cuda.memory_allocated())
    linear = nn.Linear(in_features=1, out_features=1, bias=False).cuda()
    print(torch.cuda.memory_allocated())

if __name__ == '__main__':
    test01()

程序输出结果:

0
512

我们前面创建的线性层不带偏置,只有一个参数,占用的显存应该是 4 字节,为什么这里是 512 字节?原因是 PyTorch 分配显存时是按照 512 倍数分配,也就是按块分配。为啥这样?不怕显存浪费?这也是从效率角度考虑的,按块分配便于内存管理,尽可能避免内存碎片。

import torch
import torch.nn as nn

def test02():
    print(torch.cuda.memory_allocated())
    linear = nn.Linear(in_features=128, out_features=1, bias=False).cuda()
    print(torch.cuda.memory_allocated())


if __name__ == '__main__':
    test02()

输出结果仍然是 512 字节,如果把 in_features 128 换成 129,那么就会分配 1024 字节的显存。注意一个参数的大小是 4 字节。

思考:下面的模型占用多大显存?

import torch
import torch.nn as nn


class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        self.linear1 = nn.Linear(1, 1, bias=False)
        self.linear2 = nn.Linear(1, 1, bias=False)

    def forward(self, inputs):
        inputs = self.linear1(inputs)
        inputs = self.linear2(inputs)
        return inputs


def test03():
    print(torch.cuda.memory_allocated())
    model = Net().cuda()
    print(torch.cuda.memory_allocated())


if __name__ == '__main__':
    test03()

程序输出结果是:

0
1024
  1. 前向和反向计算 {#title-2} =====================

网络模型在进行前向计算时会保存中间结果,为啥要保存?就是反向计算求梯度时需要用到这些中间结果。反向计算后得到的梯度值是需要显存来存储,所以,正向和反向计算都会占用显存。

另外,输入的 batch_size 越大,占用的显存越大。

import torch
import torch.nn as nn


def test():

    print(torch.cuda.memory_allocated())

    model = nn.Linear(1, 1).cuda()
    print(torch.cuda.memory_allocated())

    # 前向计算
    # 5120 = 1024 + 4096(1024 个输入大小)
    inputs = torch.randn(size=(1024, 1)).cuda()
    print(torch.cuda.memory_allocated())

    # 正向计算需要缓存中间计算结果(outputs)
    # 注意:用变量承接相当于缓存了中间结果
    # 9216 = 5120 + 4096(1024 个缓存结果)
    outputs = model(inputs)
    print(torch.cuda.memory_allocated())

    # 计算损失
    # 9728 = 9216 + 512 缓存损失结果
    loss = torch.mean(outputs)
    print(torch.cuda.memory_allocated())

    # 反向计算
    # 10752 = 9728 + 512 保存梯度值
    loss.backward()
    print(torch.cuda.memory_allocated())


if __name__ == '__main__':
    test()

程序执行结果:

0
1024
5120
9216
9728
10752

反向传播之后,可以释放 outputs、loss 这些变量。

  1. 优化方法统计量 {#title-3} =====================

不同的优化方法中会存在一些统计量。例如:对于 SGD 会记录每个参数的历史移动平均梯度动量,Adam 优化方法中会记录每个参数的一阶、二阶梯度动量。这些在训练过程中,也是需要占用一定的显存,并且参数量越大,这些优化方法占用的显存就越大。

import torch
import torch.nn as nn
import torch.optim as optim


def test():

    # 0
    print(torch.cuda.memory_allocated())

    # 512
    model = nn.Linear(1, 1, bias=False).cuda()
    print(torch.cuda.memory_allocated())

    # 1024
    inputs = torch.randn(size=(1, 1)).cuda()
    print(torch.cuda.memory_allocated())

    # 1536
    outputs = model(inputs)
    print(torch.cuda.memory_allocated())

    # 2048
    loss = torch.mean(outputs)
    print(torch.cuda.memory_allocated())

    # 2560
    loss.backward()
    print(torch.cuda.memory_allocated())

    # 3584
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    optimizer.step()
    print(torch.cuda.memory_allocated())


if __name__ == '__main__':
    test()

程序执行结果:

0
512
1024
1536
2048
2560
3584

SGD 如果设置 momentum 的话,内部会对每个参数记录一个历史梯度。Adam 则记录的数据较多一些。所以,Adam 的显存占用会更多一些。

赞(0)
未经允许不得转载:工具盒子 » 显存使用分析(PyTorch)