CoreML 层速度分析

在CoreML的实际应用中,很多场景下我们对模型的处理速度都有很严格的要求。这就要求我们在设计模型时,就要谨慎控制每一层的耗时。下面我将在真机上实测CoreML一些比较耗时的层在不同参数下运行时间,为设计模型提供一些参考。

理论参考:How fast is my model?
测试设备:iPhoneX
环境配置:python3.6、tensorflow1.5、Keras2.1.6、numpy1.15.4、coremltools2.0、Xcode10.1

搭建测量环境

首先建立一个简单的Keras模型:

import keras

def create_model():
    inp = keras.layers.Input(shape=(128, 128, 3))
    //需要测试的层,这里是卷积层Conv2D
    x = keras.layers.Conv2D(64, (3, 3), strides=(1, 1),padding='valid', name='conv1', use_bias=True)(x)
    return keras.models.Model(inp, x)

model = create_model()
model.compile(loss="categorical_crossentropy", optimizer="Adam",
              metrics=["accuracy"])
model.summary()

我们的目的仅仅是测试耗时,模型没有任何实际意义,所以这里不做训练,配置权重为随机数:

W = model.get_weights()
np.random.seed(12345)
for i in range(len(W)):
    W[i] = np.random.randn(*(W[i].shape)) * 2 - 1
model.set_weights(W)

最后把模型转换为CoreML模型,保存mlmodel文件,推荐把操作和参数写在名字里以便于比对。

coreml_model = coremltools.converters.keras.convert(
                                                    model,
                                                    input_names="image",
                                                    image_input_names="image",
                                                    output_names="output")

//保存mlmodel,把操作和参数写在名字里以便于比对。
coreml_model.save('Conv2D_3_3_3_128_128_64.mlmodel')

将以上代码整理到一个task.py文件中(完整代码后文中会给出),在终端中运行:

localhost:CoreMLtest vyyv$ python3 task.py

以下是返回结果

Using TensorFlow backend.
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 128, 128, 3)       0         
_________________________________________________________________
conv1 (Conv2D)               (None, 126, 126, 64)      1792      
=================================================================
Total params: 1,792
Trainable params: 1,792
Non-trainable params: 0
_________________________________________________________________
2019-01-07 14:50:26.523392: I tensorflow/core/platform/cpu_feature_guard.cc:137] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.2 AVX AVX2 FMA
0 : input_1, 
1 : conv1, 
/var/folders/2k/n498pw5d4qb41y0_wc4vmrcm0000gp/T/tmpj6ik2p3s.mlmodel
Input name(s) and shape(s): 
image : (C,H,W) = (3, 128, 128) 
Neural Network compiler 0: 100 , name = conv1, output shape : (C,H,W) = (64, 126, 126) 

至此已成功生成一个mlmodel文件,你可以在task.py所在文件夹中找到它。
下一步建立一个简单的Xcode工程,用以测试模型运行速度。

import UIKit
import CoreML
import os.signpost

class ViewController: UIViewController {
    
    lazy var input_:MLFeatureProvider = {
        var image = UIImage.init(named: "5.jpg")
        image = image?.resize(to: CGSize(width: 128, height: 128))
        let input_ = ModelInput(image: image!.buffer!)
        return input_
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func test(){
        let model =   Conv2D_3_3_3_128_128_64().model
        runModel(model: model)
    }
    
    func runModel(model:MLModel) {
        let starttimeInterval: TimeInterval = Date().timeIntervalSince1970
        let start = CLongLong(round(starttimeInterval*1000))
    
        if let output = try? model.prediction(from:input_) {
            print("finish")
        } else {
            print("error")
        }
        
        let endtimeInterval: TimeInterval = Date().timeIntervalSince1970
        let end = CLongLong(round(endtimeInterval*1000))
        
        print("time: \(end-start)")
    }
}

/// Model Prediction Input Type
class ModelInput : MLFeatureProvider {
    
    /// Input image of scene to be classified as color (kCVPixelFormatType_32BGRA) image buffer, 224 pixels wide by 224 pixels high
    var image: CVPixelBuffer
    
    var featureNames: Set {
        get {
            return ["image"]
        }
    }
    
    func featureValue(for featureName: String) -> MLFeatureValue? {
        if (featureName == "image") {
            return MLFeatureValue(pixelBuffer: image)
        }
        return nil
    }
    
    init(image: CVPixelBuffer) {
        self.image = image
    }
}

其中用到的图片转换操作:

import UIKit

extension UIImage
{
    var buffer: CVPixelBuffer? {
        let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary
        var pixelBuffer: CVPixelBuffer?
        let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(self.size.width), Int(self.size.height), kCVPixelFormatType_32ARGB, attrs, &pixelBuffer)
        guard (status == kCVReturnSuccess) else {
            return nil
        }
        
        CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
        let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!)
        
        let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
        let context = CGContext(data: pixelData, width: Int(self.size.width), height: Int(self.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)
        
        context?.translateBy(x: 0, y: self.size.height)
        context?.scaleBy(x: 1.0, y: -1.0)
        
        UIGraphicsPushContext(context!)
        self.draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
        UIGraphicsPopContext()
        CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
        
        return pixelBuffer
    }
    
    func resize(to newSize: CGSize) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(CGSize(width: newSize.width, height: newSize.height), true, 1.0)
        self.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height))
        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        
        return resizedImage
    }
}

连接iPhoneX并运行APP,可以在Xcode调试信息窗口中可得到如下信息

finish
time: 10

上面的time: 10就是模型运行耗费时间,10毫秒。

要注意到在这个简单的模型中,输入层和输出层也是要消耗时间的,消耗时间与输入输出数据大小相关,为了精确测量特定层消耗的时间我们还要引入一个coreML自定义层,因为自定义层的起止时间可以用os_signpost标记监控,所以在需测量层前后各放置一个自定义层就可以把耗时计算出来。

注:这里有一个漏洞,为了使用os_signpost标记监控,我们加入的是cup运算层,gpu和cpu之间的数据交换要消耗时间,如果是连续的gpu运算,这部分时间可以节约。

在原模型中加入一个Swish激活函数,原理和方法请参阅Custom Layers in Core ML或Core ML中的自定义层(译)

添加自定义Swish激活函数后的task.py完整代码:

import keras
import numpy as np
import coremltools
def convert_lambda(layer):
    if layer.function == swish:
        params = coremltools.proto.NeuralNetwork_pb2.CustomLayerParams()
        params.className = "Swish"
        params.description = "A fancy new activation function"
        return params
    else:
        return None

def swish(x):
    return keras.backend.sigmoid(x) * x

def create_model():
    inp = keras.layers.Input(shape=(128, 128, 3))
    x = keras.layers.Lambda(swish)(inp)
    x = keras.layers.Conv2D(64, (3, 3), strides=(1, 1),padding='valid', name='conv1', use_bias=True)(x)
    x = keras.layers.Lambda(swish)(x)
    return keras.models.Model(inp, x)

model = create_model()
model.compile(loss="categorical_crossentropy", optimizer="Adam",
              metrics=["accuracy"])
model.summary()

W = model.get_weights()
np.random.seed(12345)
for i in range(len(W)):
    W[i] = np.random.randn(*(W[i].shape)) * 2 - 1
model.set_weights(W)

coreml_model = coremltools.converters.keras.convert(
                                                    model,
                                                    input_names="image",
                                                    image_input_names="image",
                                                    output_names="output",
                                                    add_custom_layers=True,
                                                    custom_conversion_functions={ "Lambda": convert_lambda })
coreml_model.save('R_Conv2D_3_3_3_128_128_64.mlmodel')

在Xcode工程中加入Swish.swift,注意这里要注释掉调运GPU运算的代码encode,我们使用CPU运算以方便使用os_signpost。

import Foundation
import CoreML
import Accelerate

@objc(Swish) class Swish: NSObject, MLCustomLayer {
    let swishPipeline: MTLComputePipelineState
    
    required init(parameters: [String : Any]) throws {
       // print(#function, parameters)
        let device = MTLCreateSystemDefaultDevice()!
        let library = device.makeDefaultLibrary()!
        let swishFunction = library.makeFunction(name: "swish")!
        swishPipeline = try! device.makeComputePipelineState(
            function: swishFunction)
        
        super.init()
    }
    
    func setWeightData(_ weights: [Data]) throws {
        //print(#function, weights)
    }
    
    func outputShapes(forInputShapes inputShapes: [[NSNumber]]) throws
        -> [[NSNumber]] {
       //     print(#function, inputShapes)
            return inputShapes
    }
    
    func evaluate(inputs: [MLMultiArray], outputs: [MLMultiArray]) throws {
        print("swish")
        let log = OSLog(subsystem: "Swish", category: OSLog.Category.pointsOfInterest)
        os_signpost(OSSignpostType.begin, log: log, name: "Swish cpu")
        for i in 0..(OpaquePointer(input.dataPointer))
            let optr = UnsafeMutablePointer(OpaquePointer(output.dataPointer))
            
            // output = -input
            vDSP_vneg(iptr, 1, optr, 1, vDSP_Length(count))
            
            // output = exp(-input)
            var countAsInt32 = Int32(count)
            vvexpf(optr, optr, &countAsInt32)
            
            // output = 1 + exp(-input)
            var one: Float = 1
            vDSP_vsadd(optr, 1, &one, optr, 1, vDSP_Length(count))
            
            // output = x / (1 + exp(-input))
            vvdivf(optr, iptr, optr, &countAsInt32)
        }
        os_signpost(OSSignpostType.end, log: log, name: "Swish cpu")
    }
    
   /* func encode(commandBuffer: MTLCommandBuffer,
                inputs: [MTLTexture], outputs: [MTLTexture]) throws {
        if let encoder = commandBuffer.makeComputeCommandEncoder() {
            for i in 0..

并修改ViewController中的runModel函数

func runModel(model:MLModel) {
        let log = OSLog(subsystem: "main", category: OSLog.Category.pointsOfInterest)
        os_signpost(OSSignpostType.begin, log: log, name: "main")
        
        if let output = try? model.prediction(from:input_) {
            print("finish")
        } else {
            print("error")
            
        }
        os_signpost(OSSignpostType.end, log: log, name: "main")
    }

这次我们在Instruments中运行APP

运行Product->Profile,选择Blank,点击右上角的+号,在列表中选择os_signpost,至此界面如下:

点击红色运行按钮,并在iphone中多次运行测试模型,可以看到如下显示:

拉伸显示区域高度,可以在左侧显示我们加入的os_signpost的名字,右侧时间轴也可以放大,以便观察运行时的细节:

从上图中可以看到,第四行是我们运行一次模型的时间,第二行是两次swish层运行的时间,它们之间的时间即为我们要测量层运行消耗的时间,拖拽鼠标即可测量。

os_signpost还有很多实用的功能,就不再这里进行扩展了,可自行探索。

至此,测量神经网络每一层运行耗时的准备工作已经完成,下面开始实测。

注:本文中的所有测量数据均为多次测量的平均值。受条件所限,测量结果难免会有偏差,不要直接使用结果数据。我们关心的是耗费时间的量级,以及不同参数下速度的比较关系。

卷积层

我参与的几个神经网络模型都与图像相关,这些模型中遇到做多、最耗时的就是卷积层,我们这次测量工作也主要围绕卷积层进行。

二维卷积层

二维卷积层计算量公式为

MACC = K × K × Cin × Hout × Wout × Cout
  • Hout × Wout 对应输出特征图中的像素数目
  • K x K 卷积核的宽度和长度
  • Cin 输入通道数
  • Cout 是卷积核的数目,即输出的维度

Keras 二维卷积层公式为:

keras.layers.convolutional.Conv2D(filters, kernel_size, strides=(1, 1), padding='valid', data_format=None, dilation_rate=(1, 1), activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None)

和计算量有关的主要参数:

  • filters 卷积核的数目,输出的维度,即运算量的公式中的Cout
  • kernel_size 卷积核的宽度和长度,即运算量的公式中的K x K
  • strides 卷积的步长,与padding一起影响运算量的公式中Hout × Wout的数值
  • padding 补0策略,为“valid”, “same”
首先测量kernel_size取不同值时,卷积层运行时间。

设定输入图像shape=(256, 256, 3), filters=64,strides=(1, 1),padding=same,此时输出特征Hout × Wout与输入图像相同为256*256。

K MACC ms MACC/ms params
1 1x1x3x256x256x64=12582912 12 1.0m (million) 256
3 3x3x3x256x256x64=113246208 14 8.1m 1792
5 5x5x3x256x256x64=314572800 21 15m 4864
7 7x7x3x256x256x64=616562688 22 28m 9472

注:MACC/ms单位是million,不是秒,有些读者误会了。

设定输入图像shape=(128, 128, 3), filters=64,strides=(1, 1),padding=same,此时输出特征Hout × Wout与输入图像相同为128*128。

K MACC ms MACC/ms params
1 1x1x3x128x128x64=3145728 4.5 0.7m 256
3 3x3x3x128x128x64=28311552 5.2 5.4m 1792
5 5x5x3x128x128x64=78543200 6.0 13.1m 4864
7 7x7x3x128x128x64=154140672 6.2 24.8m 9472

可以看到,随着K增大,模型的运算速率明显增加,CoreML应该是针对卷积核大小做了速度上的优化。

测量输出特征Hout × Wout取不同值时,卷积层运行时间。

设定filters=64,kernel_size=(3, 3),strides=(1, 1),padding=same,此时输出特征Hout × Wout与输入图像相同,改变输入图像宽高,输出特征大小也将随之改变。

filters MACC ms MACC/ms params
64x64 3x3x3x64x64x64=7077888 1.9 3.7m 1792
128x128 3x3x3x128x128x64=28311552 5.2 5.4m 1792
256x256 3x3x3x256x256x64=113246208 14 8.1m 1792
512x512 3x3x3x512x512x64=452984832 52 8.7m 1792
测量filters取不同值时,卷积层运行时间。

设定输入图像shape=(512, 512, 3), kernel_size=(3, 3),strides=(1, 1),padding=same,此时输出特征Hout × Wout与输入图像相同为512*512。

filters MACC ms MACC/ms params
16 3x3x3x512x512x16=113246208 14 8.1m 448
32 3x3x3x512x512x32=226492416 29 7.8m 896
64 3x3x3x512x512x64=452984832 52 8.7m 1792
128 3x3x3x512x512x128=905969664 90 10.1m 3584

设定输入图像shape=(128, 128, 3), kernel_size=(3, 3),strides=(1, 1),padding=same,此时输出特征Hout × Wout与输入图像相同为128*128。

filters MACC ms MACC/ms params
64 3x3x3x128x128x64=28311552 5 5.7m 1792
128 3x3x3x128x128x128=56623104 8 7.1m 3584
256 3x3x3x128x128x256=113246208 15 7.5m 7168
512 3x3x3x128x128x512=226492416 28 8.1m 14336
测量输入通道取不同值时,卷积层的运行时间

如果输入层为图片,一般输入通道数为1或3。要改变输入通道数需要在前面加一层卷积。

def create_model():
    inp = keras.layers.Input(shape=(128, 128, 3))
    x = keras.layers.Conv2D(1024, (3, 3), strides=(1, 1),padding='same', name='conv1', use_bias=True)(inp)
    x = keras.layers.Lambda(swish)(x)
    x = keras.layers.Conv2D(128, (3, 3), strides=(1, 1),padding='same', name='conv2', use_bias=True)(x)
    x = keras.layers.Lambda(swish)(x)
    return keras.models.Model(inp, x)

设定输入图像shape=(64, 64, 3), 通过第一层卷积把通道数改变为128、256、512、1024,第二层卷积的kernel_size=(3, 3),strides=(1, 1),padding=same,此时输出特征Hout × Wout与输入图像相同为64*64。

Cin MACC ms MACC/ms params
16 3x3x16x64x64x128=75497472 4.2 18.0m 18560
32 3x3x32x64x64x128=150994944 6 25.2m 36992
64 3x3x64x64x64x128=301989888 9.5 31.8m 73856
128 3x3x128x64x64x128=603979776 17 35.5m 147584
256 3x3x256x64x64x128=1.20796e9 26 46m 295040
512 3x3x512x64x64x128=2.415919e9 45 54m 589952
1024 3x3x1024x64x64x128=4.831838e9 70 69m 1179776
深度卷积 DepthwiseConv2D

CoreML中的卷积层有一个参数是nGroups,当nGroups不等于1的时候,这个卷积层就是一个分组卷积。keras.applications.mobilenet.DepthwiseConv2D通过coremltools就会被转换为kernelChannels=1的分组卷积层,nGroups=上一层的输出通道数。

池化层

池化层的计算量不能使用MACC统计,需要使用FLOPs。它的计算量公式为

Hin × Win x Cin

下面是对池化层速度的测量(这里测量的是TinyYOLO的6个池化层)

Hin × Win x Cin ms FLOPS
416x416x16=2768896 4.1 0.67b
208x208x32=1384448 2.7 0.51b
104x104x64=692224 2 0.35b
52x52x128=346112 1.2 0.29b
26x26x256=173056 1 0.17b
13x13x512=86528 1 0.09b
TinyYOLO

TinyYOLO是一个非常简单的模型结构,包含9个卷积、6个池化层和其他一些层。

池化层的时间前面已经列出过,下面列出卷积层的时间开销

K × K × Cin × Hout × Wout × Cout ms MACC/ms
3x3x3x416x416x16=74760192 9.1 8.2m
3x3x16x208x208x32=199360512 8.1 24.6m
3x3x32x104x104x64=199360512 6.7 29.8m
3x3x64x52x52x128=199360512 5.9 33.8m
3x3x128x26x26x256=199360512 6.5 30.7m
3x3x256x13x13x512=199360512 8.6 23.2m
3x3x512x13x13x1024=797442048 22 36.2m
3x3x1024x13x13x1024=1.594884e9 32 49.8m
1x1x1024x13x13x125=21632000 1.6 13.5m

你可能感兴趣的:(CoreML 层速度分析)