马上注册,享用更多功能,让你轻松玩转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,该函数在每一层的前向传播时被调用。通过在这个回调函数中获取每一层的中间计算结果大小,我们能够了解模型在不同层的显存占用情况。
【以上文字内容仅代表个人理解,如有其他建议,欢迎在评论区交流】 |