性能指标
FLOPS:浮点运算次数。
MADD:表示一次乘法和一次加法,这可以粗略认为:MADD=2 * Flops,即((输出一个元素所经历的乘法次数)+(输出一个元素所经历的加法的个数)) * (输出总共的元素的个数)
MEMREAD:网络运行时,从内存中读取的大小,即输入的特征图大小 + 网络参数的大小
MEMWRITE:网络运行时,写入到内存中的大小,即输出的特征图大小
模型类型
卷积神经网络
在经典卷积神经网络中,输入特征图通常为(Htimes 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]深度学习模型推理速度/吞吐量计算