理性分析不同模型的性能指标
性能指标
FLOPS:
浮点运算次数。
MADD:
表示一次乘法和一次加法,这可以粗略认为:MADD=2 * Flops,即((输出一个元素所经历的乘法次数)+(输出一个元素所经历的加法的个数)) * (输出总共的元素的个数)
MEMREAD:
网络运行时,从内存中读取的大小,即输入的特征图大小 + 网络参数的大小
MEMWRITE:
网络运行时,写入到内存中的大小,即输出的特征图大小
模型类型
卷积神经网络
在经典卷积神经网络中,输入特征图通常为\(H\times W\)(这里假设\(H,W\)一致),输入通道数目为\(C_i\),卷积核输入深度通常和输入特征图的输入通道保持一致,其核大小通常为\(K\),输出通道(可以理解为输出深度)通常为\(C_{out}\),除此以外需要考虑填充大小\(P\)和步长\(S\)。基于上述条件,特征图输出大小\(M\)满足:
在每一次卷积过程中,每个卷积核与输入特征图上当前步对应的窗口内元素进行逐通道、逐元素相乘并累加;重复多次操作,直至卷积核完整遍历输入特征图,每个卷积层运算次数flops
满足下述关系:
依据层内相乘,层间相加的准则,经典卷积神经网络模型所有卷积层的时间复杂度满足关系如下(\(N\)为卷积层的数目):
下面通过一个案例,来具体分析:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 2, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(2)
self.relu1 = nn.ReLU()
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu1(x)
return x
net = Net()
stat(net, (3, 500, 500))
# 输入通道为3 输入特征图大小为500
# 输出特征图大小为(500-7+6)/2+1=250
# Flops=250*250*3*2*7*7=18375000
# MADD=(250*250*2)*((7*7*3)+(7*7*3-1))=36625000
# MemREAD= ((500 * 500 * 3) + (7 * 7 * 3 * 2)) * 4 = 3001176.0 【认为每个参数为FP32,满足四个字节,因此乘4】
# MEMW:250 * 250 * 2 * 4 = 500000
FLOPS/PARAMS参数计算工具
下面给出thop
的使用方法,thop
只能计算输入为一个矩阵的模型的参数量和FLOPs,实际中的模型可能存在多个输入,如以下案例:
out = model(detections,boxs,grids,masks,captions)
# 其中作为模型传入参数的是五个矩阵,其尺寸为:
detections.shape = (bs,50,2048)
boxs.shape = (bs,50,4)
grids.shape = (bs,49,2048)
masks.shape = (bs,50,49)
captions.shape = (bs,19)
想要计算这样一个模型的参数量和计算量,需要考虑一种新的方式。我在这里给出的方案是:构建一个新的,接收单矩阵输入的类。代码如下:
import torch.nn as nn
class func(nn.Module):
def __init__(self,object):
super(func,self).__init__()
self.model = object
def forward(self,a):
detections = torch.randn(1,50,2048).float()
boxs = torch.randn(1,50,4).float()
grids = torch.randn(1,49,2048).float()
masks = torch.randn(1,50,49).float()
captions = torch.tensor([0,100,105,2,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1]).unsqueeze(0)
print(captions.shape)
out = self.model(detections,boxs,grids,masks,captions)
return out
可以看到,该类继承自nn.Module,以确保该类可以被声明为一个模型。接着在类的初始化函数中,将待计算模型的对象作为参数传入,并赋值给self.model;在forward函数中,规定其必须接收一个参数(实际上我们并不会使用这个接收到的参数),并通过torch内置的随机函数产生需要形状的张量(之所以特殊对待captions是因为model要求该参数的元素为整型)。
最后,就可以进行参数量和FLOPs的计算,代码如下:
model = Transformer(text_field.vocab.stoi['<bos>'],
encoder, decoder, args=args)
use_model = func(model)
input = torch.randn(1, 3, 224, 224)
flops, params = profile(use_model, inputs=(input))
print("FLOPs=", str(flops/1e9) + '{}'.format("G"))
print("params=", str(params/1e6) + '{}'.format("M"))
其中,第一行声明Transformer类,并将其对象命名为model;第二行声明上面定义的func类,将model作为声明类时的传入参数;第三行随机生成一个矩阵;第四行调用thop中的profile方法对func类的参数量和运算量进行计算,实际上等价于对Transformer类的参数量和运算量计算。
吞吐和时延
吞吐量:完成一个特定任务的速率(单位时间内完成的任务数目,衡量指标为bits/second
,Bytes/second
)。对神经网络而言,吞吐量可以设置为每秒处理的图片数量或语音数量。
延时:完成一个任务需要花费的时间。
吞吐量和延时并不是严格的反比关系。吞吐量可以认为是一个系统并行处理的任务量,延时是一个系统串行处理一个任务所花费的时间
optimal_batch = 160000 # 基于本案例中的模型所测试出的最佳batch为160000
# while True: # 这里是确定最佳batch的大小 出现cuda显存不足前认为是最佳batch
# x = torch.randn(optimal_batch, 3, 28, 28).to(device='cuda')
# _ = net(x)
# print("当前测试批量为:", optimal_batch) if optimal_batch % 1000 == 0 else None
# optimal_batch += 100
input = torch.randn(optimal_batch, 3, 28, 28).to(device='cuda')
repetitions = 100
total_time = 0
with torch.no_grad():
for rep in range(repetitions):
starter, ender = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True)
starter.record()
_ = net(input)
ender.record()
torch.cuda.synchronize()
curr_time = starter.elapsed_time(ender) / 1000
total_time += curr_time
Throughput = (repetitions * optimal_batch) / total_time
print('Final Throughput:', Throughput)
参考文献
[1] 面试宝典笔记:卷积计算过程中的FLOPs
[2] 通过thop计算模型的参数量与运算量(FLOPs)
[3] THOP+torchstat 计算PyTorch模型的FLOPs,问题记录与解决
[4]吞吐和时延
[5]深度学习模型推理速度/吞吐量计算