返回顶部

[学习经验] 如何计算模型显存大小?代码实现

[复制链接]
迪迦奥特曼Lv.7 显示全部楼层 发表于 2023-10-13 16:16:55 |阅读模式 打印 上一主题 下一主题

马上注册,享用更多功能,让你轻松玩转AIHIA梦工厂!

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 迪迦奥特曼 于 2023-10-13 04:21 PM 编辑

                                                                                       
作者:刘一手
_____________________________________________________________________

在机器学习领域,模型的性能优化是一个永恒的话题。其中,计算模型显存大小是至关重要的一环,特别是在硬件资源受限的情况下。下面深入探讨如何准确计算模型显存大小,并提供相应的代码实现。


1、显存大小的重要性                                                                              
显存大小直接影响着模型的训练和推理过程。过大的模型可能导致内存不足,从而无法在GPU上运行,而过小的模型可能无法满足任务需求。

2、模型显存的组成                                                                           
模型显存主要由以下几个部分组成:
(1)模型参数:权重和偏置的存储空间;
(2)梯度:反向传播过程中需要存储的梯度信息;
(3)中间计算结果:前向传播过程中各层的中间计算结果。
                                                                                       
3、计算模型显存大小的方法
(1)参数大小
模型参数的大小是显存消耗的一个主要因素。可以通过以下代码来计算:
# 导入PyTorch库
import torch
import torchvision.models as models

"""
# 定义一个函数calculate_parameters,该函数接受一个模型作为参数,
# 然后通过遍历模型的所有参数,计算它们的元素总数,并返回总数。
"""
def calculate_parameters(model):
    return sum(p.numel() for p in model.parameters())

# 示例:创建ResNet18模型实例
resnet18 = models.resnet18()

# 调用calculate_parameters函数,计算ResNet18模型的参数总数
params_size = calculate_parameters(resnet18)

# 打印输出ResNet18模型的参数总数
print(f"模型参数大小: {params_size} 个")
                                                                           
输出:
模型参数大小: 11689512
                                                                                 
(2)梯度大小
在训练过程中,梯度需要存储在显存中以进行反向传播,计算梯度大小的代码如下:
# 导入PyTorch库
import torch
import torchvision.models as models


# 定义一个函数,用于计算模型在给定输入大小下的梯度总数
def calculate_gradients(model, input_size):
    # 生成一个符合输入大小要求的随机输入张量
    input_tensor = torch.randn(input_size)

    # 将输入张量传递给模型,执行前向传播
    output = model(input_tensor)

    # 计算模型输出的总和,作为损失
    loss = output.sum()

    # 反向传播,计算梯度
    loss.backward()

    # 返回模型各参数的梯度总数,注意排除梯度为None的参数
    """
    p.grad.numel() 是 PyTorch 中用于获取张量 p.grad 中元素数量的方法。

    p 是模型中的参数(Parameter)对象。
    p.grad 是该参数的梯度(gradient)张量。
    numel() 是 PyTorch 中的方法,用于返回张量中元素的总数。
    """
    return sum(p.grad.numel() for p in model.parameters() if p.grad is not None)


# 示例:创建ResNet18模型实例
resnet18 = models.resnet18()

# 调用calculate_gradients函数,计算ResNet18模型在给定输入大小下的梯度总数
gradient_size = calculate_gradients(resnet18, (1, 3, 224, 224))

# 打印输出计算得到的梯度总数
print(f"梯度大小: {gradient_size} 个")

输出:
梯度大小: 11689512
                                                                           
(3)中间计算结果的大小
中间计算结果的大小也会影响显存的使用情况。计算中间计算结果大小需要通过Hook机制来获取模型每一层的输出。在PyTorch中,可以通过注册forward hook来实现。
# 导入PyTorch库
import torch
import torchvision.models as models

# 定义一个回调函数,用于在每一层前向传播时打印输出大小
def forward_hook(module, input, output):
    print(f"{module.__class__.__name__}: {output.size()}")

# 创建ResNet18模型实例
resnet18 = models.resnet18()

# 注册hook,选择模型的第一个卷积层(layer1)作为例子
hook_handle = resnet18.layer1.register_forward_hook(forward_hook)

# 创建一个随机输入张量,模拟模型的输入
input_tensor = torch.randn(1, 3, 224, 224)

# 示例:执行前向传播,观察中间计算结果大小
output = resnet18(input_tensor)

# 移除hook,确保不再影响后续前向传播
hook_handle.remove()
                                                                              

输出:
Sequential: torch.Size([1, 64, 56, 56])
                                                                                 
4、问题解答                                                                                 
(1)为什么上面程序输出的参数大小和梯度大小数值相同,他们是一直相同的吗?
在上面的代码中,参数大小和梯度大小的数值相同,这是因为在示例中我们使用了一个随机的输入张量执行了前向传播,并计算了梯度。
具体来说,以下两个部分导致了相同的数值:                                                                     
1)参数大小的计算:
params_size = calculate_parameters(resnet18)
print(f"模型参数大小: {params_size} 个")
                                                                              

这段代码通过calculate_parameters函数计算了模型的参数大小,包括权重和偏置。这个数值是在模型初始化时确定的,与具体的输入无关。
                                                                                 
2)梯度大小的计算
gradient_size = calculate_gradients(resnet18, (1, 3, 224, 224))
print(f"梯度大小: {gradient_size} 个")
                                                                              

这段代码通过执行前向传播、计算损失、反向传播,然后计算梯度的方式得到的梯度大小。


解释:
在训练过程中,模型的参数根据输入数据的梯度进行更新。如果输入数据导致较大的梯度,那么在训练中,这些较大的梯度将影响参数的更新,从而可能导致参数的变化更加剧烈。

在示例中提到的情境中,为了计算梯度大小,我们使用了一个随机生成的输入张量 input_tensor = torch.randn(input_size)。这个随机输入可能没有引起足够大的梯度变化,因此在示例中观察到的参数大小和梯度大小相同。在实际应用中,如果输入数据的变化足够大,那么相应的梯度大小也会随之变化。

总体来说,在实际应用中,参数大小和梯度大小通常是不同的。参数大小在模型初始化时固定,而梯度大小取决于具体的输入数据以及损失函数的设计。在训练过程中,随着不同输入数据的反复迭代,梯度大小会变化梯度大小受到输入数据的影响,而示例中可能由于使用了随机输入,没有充分展示梯度与输入数据之间的关系。在实际训练中,使用真实的训练数据会导致更有意义的梯度大小。

                                                                                
下面以一个简单的例子,来观察梯度与输入数据之间的变化关系:
import torch
import torchvision.models as models

# 定义一个函数,用于观察输入数据和梯度的变化关系
def observe_gradients(model, input_tensor, target):
    # 设置模型为训练模式
    model.train()

    # 将输入数据和目标数据转换为可求梯度的张量
    input_tensor.requires_grad_(True)
    target.requires_grad_(True)

    # 将输入数据传递给模型,执行前向传播
    output = model(input_tensor)

    # 计算损失
    loss = torch.nn.functional.mse_loss(output, target)

    # 执行反向传播,计算梯度
    loss.backward()

    # 获取输入数据的梯度
    input_gradient = input_tensor.grad.clone()

    return input_tensor, input_gradient

# 示例:创建ResNet18模型实例
resnet18 = models.resnet18()

# 示例:创建随机输入数据和随机目标数据
input_tensor = torch.randn(1, 3, 224, 224, requires_grad=True)
target = torch.randn(1, requires_grad=True)

# 示例:观察梯度与输入数据之间的变化
observed_input, input_gradient = observe_gradients(resnet18, input_tensor, target)

# 打印输出结果
print("输入数据:")
print(observed_input)
print("\n输入数据的梯度:")
print(input_gradient)
                                                                              

输出:
输入数据:
tensor([[[[-0.8467,  0.7795,  0.6696,  ..., -1.0453,  0.8671,  1.1189],
          [-0.4570, -0.8333,  0.1528,  ..., -0.4692, -0.3198, -0.4729],
          [-2.0603, -0.1298,  1.8279,  ...,  0.3829,  0.9355, -0.7722],
          ...,
          [[-0.4432,  0.3189,  0.8394,  ..., -0.1833, -0.6496, -0.6611],
          [ 0.0080,  1.3797,  0.0944,  ...,  0.6649,  0.2195,  0.0090],
          [-0.2289,  0.2747, -0.2267,  ...,  0.6175, -0.0395, -0.6550],
          ...,
          [-0.0848, -1.4528,  0.7229,  ...,  0.2761, -0.0322, -1.4346],
          [-0.6599,  0.0808,  0.6114,  ...,  0.6414, -0.7882,  0.0417],
          [ 0.3284,  0.0395, -0.7621,  ...,  0.6865,  0.4610, -1.2907]]]],
       requires_grad=True)

输入数据的梯度:
tensor([[[[-2.0091e-05, -2.3195e-05, -4.4952e-05,  ...,  4.5826e-06,
            1.0580e-06,  2.2323e-06],
          [ 4.1277e-05,  3.0696e-05,  4.7199e-05,  ..., -7.4830e-06,
           -1.1108e-06,  3.6681e-06],
          ...,
          [-9.5528e-06, -8.2929e-06, -5.8567e-06,  ...,  5.9600e-06,
           -1.3356e-05, -2.8988e-06],
          [-2.5329e-05,  1.2746e-05, -3.3779e-07,  ...,  9.3909e-06,
            2.2129e-06, -5.8344e-06],
          [-2.8233e-06,  2.4108e-05,  8.3690e-07,  ..., -6.2873e-07,
            2.8088e-06, -6.2653e-06]],
          ...,
          [ 5.3693e-06,  2.4184e-05,  2.2198e-06,  ..., -5.9894e-07,
           -7.6486e-06, -6.7110e-06],
          [-1.1790e-05, -5.2587e-06,  8.5440e-06,  ...,  4.4303e-06,
            6.8020e-06,  3.9100e-06],
          [ 2.4398e-05,  1.1294e-05, -1.1575e-05,  ...,  3.7088e-06,
            8.8437e-06, -9.1870e-06]]]])
                                                                           

从上面的数值可以看出,不同的输入数据对应的梯度大小是不同的。


(2)Hook机制在计算模型显存大小的中有什么作用?
在计算模型显存大小时,Hook机制可以帮助我们观察每一层的中间计算结果的大小,从而更全面地估计模型在显存中的占用情况。Hook机制允许我们注册回调函数,在模型的每一层前向传播时获取中间计算结果,这对于分析和优化模型的显存占用非常有帮助。

所谓回调函数,比如例子中的forward_hook,该函数在每一层的前向传播时被调用。通过在这个回调函数中获取每一层的中间计算结果大小,我们能够了解模型在不同层的显存占用情况。

【以上文字内容仅代表个人理解,如有其他建议,欢迎在评论区交流】
AIHIA梦工厂,共建AI人脉圈,共享AI时代美好生活!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

AIHIA梦工厂旨在建立涵盖广泛人工智能行业,包括AI芯片、AI工业应用、AI电商、AI自动驾驶、AI智慧城市、智慧农业等人工智能应用领域。梦工厂为每位AI人提供技术交流、需求对接、行业资源、招聘求职、人脉拓展等多个方面交流学习平台促进人工智能的发展和应用。
  • 官方手机版

  • 联盟公众号

  • 商务合作

  • Powered by Discuz! X3.5 | Copyright © 2023, AIHIA梦工厂
  • 苏ICP备2023025400号-1 | 苏公网安备32021402002407 | 电信增值许可证:苏B2-20231396 | 无锡腾云驾数技术服务有限公司 QQ