NCNN GPU初始化加速——cache实现

概要

 NCNN的CPU初始化速度很快,但是当使用GPU进行推理时,初始化往往要花费几秒甚至更长时间。其他框架例如MNN有载入cache的方式来进行加速,NCNN目前没有相关接口来实现加速,那么NCNN是否也可以加载cache来实现加速呢?

整体流程

通过测速以及查看NCNN的源码可以发现,在gpu.cpp源文件下的VulkanDevice::create_pipeline函数内的vkCreateComputePipelines占了相当长时间,而vkCreateComputePipelines是vulkan的一个函数,该函数可以通过载入pipelineCache来实现加速。本文所做的工作就是通过生成读取这个pipelineCache来进行加速的。

具体实现

NCNN的GPU初始化加速分为写和读两部分。

写:

因为create_pipeline这个函数会被执行多次,例如我这边两个模型需要执行171次,所以保存文件的时候需要一个计数器来对文件分别进行命名,我这边创建了一个GlobalCounter.cpp,这个源文件的内容很简单,新建一个globalCounter 变量。

int globalCounter = 0;

gpu.cpp包含这个cpp文件,#include "GlobalCounter.cpp"。

在每次保存的时候globalCounter++进行自增来区分不同的cache文件。

修改原来的vkCreateComputePipelines,改成如下:

VkPipelineCache pipelineCache;
VkPipelineCacheCreateInfo cacheCreateInfo{};
cacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
// 创建VkPipelineCache对象
VkResult result = vkCreatePipelineCache(d->device, &cacheCreateInfo, nullptr, &pipelineCache);

VkResult ret = vkCreateComputePipelines(d->device, pipelineCache, 1, &computePipelineCreateInfo, 0, pipeline);

// 将pipelineCache的值保存到本地文件
globalCounter++;
char filename[50];
sprintf(filename, "/sdcard/dcim/tmp/%d.bin", globalCounter);
FILE* fp = nullptr;
VkDeviceSize size;
VkResult res = vkGetPipelineCacheData(d->device, pipelineCache, &size, nullptr);
if (res != VK_SUCCESS) {
   NCNN_LOGE("Error getting size of pipeline cache %d\n", res);
}
void* data = malloc(size);
res = vkGetPipelineCacheData(d->device, pipelineCache, &size, data);
if (res != VK_SUCCESS) {
   NCNN_LOGE("Error getting data of pipeline cache %d\n", res);
}
fp = fopen(filename, "wb");
if (!fp) {
   NCNN_LOGE("Failed to open file for writing.\n");
}
fwrite(data, size, 1, fp);
fclose(fp);
free(data);


改完后重新编译并生成ncnn库,再在设备上运行一次。cache文件就被保存到了指定的地方。

可以按照写的方式,通过一个计数器来分别一次读取二进制文件。为了更快的进行初始化,我的想法是合并这些cache文件,通过创建一个数组,把cache文件的数据全部放到数组里去,在初始化的时候直接读数组的内容,省去了读的这个操作,实测下来相比依次读取二进制文件两个模型总共快了1s。

因此我的实现还多了一步,合并:

    if (globalCounter == 0)
    {
        VkPipelineCache pipelineCache[171];
        VkPipelineCache MergeCache;
        VkPipelineCacheCreateInfo MergecacheCreateInfo{};
        MergecacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
        VkResult result = vkCreatePipelineCache(d->device, &MergecacheCreateInfo, nullptr, &MergeCache);
        for (int i = 0; i < 171; i++)
        {
            char filename[50];
            sprintf(filename, "/sdcard/dcim/tmp/%d.bin", (i + 1));
            std::ifstream file(filename, std::ios::binary);
            file.seekg(0, std::ios::end);
            size_t fileSize = static_cast(file.tellg());
            file.seekg(0, std::ios::beg);
            std::vector cacheData(fileSize);
            file.seekg(0);
            file.read(cacheData.data(), fileSize);
            VkPipelineCacheCreateInfo cacheCreateInfo{};
            cacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
            cacheCreateInfo.initialDataSize = fileSize;
            cacheCreateInfo.pInitialData = cacheData.data();
            VkResult result = vkCreatePipelineCache(d->device, &cacheCreateInfo, nullptr, &pipelineCache[i]);
        }
        vkMergePipelineCaches(d->device, MergeCache, 171, pipelineCache);
        char filename[50];
        sprintf(filename, "/sdcard/dcim/tmp/MergeCache.bin");
        FILE* fp = nullptr;
        VkDeviceSize size;
        VkResult res = vkGetPipelineCacheData(d->device, MergeCache, &size, nullptr);
        if (res != VK_SUCCESS) {
            NCNN_LOGE("Error getting size of pipeline cache %d\n", res);
        }
        void* data = malloc(size);
        res = vkGetPipelineCacheData(d->device, MergeCache, &size, data);
        if (res != VK_SUCCESS) {
            NCNN_LOGE("Error getting data of pipeline cache %d\n", res);
        }
        fp = fopen(filename, "wb");
        if (!fp) {
            NCNN_LOGE("Failed to open file for writing.\n");
        }
        fwrite(data, size, 1, fp);
        fclose(fp);
        free(data);
    }

通过第一步写可以得知总共创建了多少cache文件,我这边一共是171个cache文件。随后利用vulkan的vkMergePipelineCaches来合并cache文件,最后将MergeCache保存到本地。因为我还是放在create_pipeline函数下,这个函数是要执行100多次的,而合并文件只需要执行一次,所以我这边通过globalCounter == 0来让它只执行一次合并操作。

合并完后,因为我们要把它放到数组里去,而MergeCache.bin是一个二进制文件,里面的内容如何复制呢?这里推荐使用HxD,它可以读取二进制文件并将其导出为c文件,它会自动把数据全部存到一个数组里去。

NCNN GPU初始化加速——cache实现_第1张图片

读:

重新生成新的ncnn文件,因为原来写的内容在读的时候并不需要。修改MergeCache.c,内容如下:

#include 

VkPipelineCache GlobalPipelineCache = 0;

unsigned char CacheData[759974] = {xxxxxxxxxxxxxxxx}

void CreateGlobalPipelineCache(VkDevice *device)
{
	VkPipelineCacheCreateInfo cacheCreateInfo{};
	cacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
	cacheCreateInfo.initialDataSize = sizeof(CacheData);
	cacheCreateInfo.pInitialData = CacheData;
	VkResult result = vkCreatePipelineCache(*device, &cacheCreateInfo, nullptr, &GlobalPipelineCache);
}

创建了一个GlobalPipelineCache 变量,并定义了创建GlobalPipelineCache的函数。CacheData是HxD转换好的数组,因为数据过多,这边用xxxx表示其内容。并将MergeCache.c重命名为PipelineCacheData.cpp。

然后在gpu.cpp下包含这个源文件,#include "PipelineCacheData.cpp"。

CreateGlobalPipelineCache我把它放在了

VulkanDevice::VulkanDevice(int device_index)
    : info(get_gpu_info(device_index)), d(new VulkanDevicePrivate(this)) 

函数最后,其实只要让CreateGlobalPipelineCache放在create_pipeline实现的前面任一函数内且保证这个函数只执行一次就可以了,我这边选择的是VulkanDevice函数下。

最后修改vkCreateComputePipelines,将原来的

VkResult ret = vkCreateComputePipelines(d->device, 0, 1, &computePipelineCreateInfo, 0, pipeline);

改为

VkResult ret = vkCreateComputePipelines(d->device, GlobalPipelineCache, 1, &computePipelineCreateInfo, 0, pipeline);

重新编译ncnn库文件,至此NCNN GPU初始化加速——cache实现就全部完成了。

初始化速度对比:

方法 初始化速度
NCNN 8s
NCNN-cache 4.6s
MNN-cache 3.1s

这里的初始化速度包含了项目的整个初始化时间,并不单单只是模型的载入。具体速度的话对于NCNN-cache 库,NCNN初始化时间:创建shader 1.5s,创建pipeline 0.3s ,上传模型2s。剩下的时间花在其他初始化上。

你可能感兴趣的:(ncnn)