显存使用分析(PyTorch)

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

  1. CUDA 运行内存
  2. 模型的固定参数
  3. 模型的前向计算
  4. 模型的反向计算
  5. 优化方法统计量

1. CUDA 运行内存

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 可以获得目前分配的内存数量。

2. 模型的固定参数

这一部分也是比较容易理解的,加载模型就是加载模型参数。所以,模型的参数会占用一部分的显存。默认情况下, 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

3. 前向和反向计算

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

另外,输入的 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 这些变量。

4. 优化方法统计量

不同的优化方法中会存在一些统计量。例如:对于 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 的显存占用会更多一些。

未经允许不得转载:一亩三分地 » 显存使用分析(PyTorch)
评论 (0)

5 + 7 =