当前位置:   article > 正文

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

ncnn gpu

概要

 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,改成如下:

  1. VkPipelineCache pipelineCache;
  2. VkPipelineCacheCreateInfo cacheCreateInfo{};
  3. cacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
  4. // 创建VkPipelineCache对象
  5. VkResult result = vkCreatePipelineCache(d->device, &cacheCreateInfo, nullptr, &pipelineCache);
  6. VkResult ret = vkCreateComputePipelines(d->device, pipelineCache, 1, &computePipelineCreateInfo, 0, pipeline);
  7. // 将pipelineCache的值保存到本地文件
  8. globalCounter++;
  9. char filename[50];
  10. sprintf(filename, "/sdcard/dcim/tmp/%d.bin", globalCounter);
  11. FILE* fp = nullptr;
  12. VkDeviceSize size;
  13. VkResult res = vkGetPipelineCacheData(d->device, pipelineCache, &size, nullptr);
  14. if (res != VK_SUCCESS) {
  15.    NCNN_LOGE("Error getting size of pipeline cache %d\n", res);
  16. }
  17. void* data = malloc(size);
  18. res = vkGetPipelineCacheData(d->device, pipelineCache, &size, data);
  19. if (res != VK_SUCCESS) {
  20.    NCNN_LOGE("Error getting data of pipeline cache %d\n", res);
  21. }
  22. fp = fopen(filename, "wb");
  23. if (!fp) {
  24.    NCNN_LOGE("Failed to open file for writing.\n");
  25. }
  26. fwrite(data, size, 1, fp);
  27. fclose(fp);
  28. free(data);

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

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

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

  1.     if (globalCounter == 0)
  2.     {
  3.         VkPipelineCache pipelineCache[171];
  4.         VkPipelineCache MergeCache;
  5.         VkPipelineCacheCreateInfo MergecacheCreateInfo{};
  6.         MergecacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
  7.         VkResult result = vkCreatePipelineCache(d->device, &MergecacheCreateInfo, nullptr, &MergeCache);
  8.         for (int i = 0; i < 171; i++)
  9.         {
  10.             char filename[50];
  11.             sprintf(filename, "/sdcard/dcim/tmp/%d.bin", (i + 1));
  12.             std::ifstream file(filename, std::ios::binary);
  13.             file.seekg(0, std::ios::end);
  14.             size_t fileSize = static_cast<size_t>(file.tellg());
  15.             file.seekg(0, std::ios::beg);
  16.             std::vector<char> cacheData(fileSize);
  17.             file.seekg(0);
  18.             file.read(cacheData.data(), fileSize);
  19.             VkPipelineCacheCreateInfo cacheCreateInfo{};
  20.             cacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
  21.             cacheCreateInfo.initialDataSize = fileSize;
  22.             cacheCreateInfo.pInitialData = cacheData.data();
  23.             VkResult result = vkCreatePipelineCache(d->device, &cacheCreateInfo, nullptr, &pipelineCache[i]);
  24.         }
  25.         vkMergePipelineCaches(d->device, MergeCache, 171, pipelineCache);
  26.         char filename[50];
  27.         sprintf(filename, "/sdcard/dcim/tmp/MergeCache.bin");
  28.         FILE* fp = nullptr;
  29.         VkDeviceSize size;
  30.         VkResult res = vkGetPipelineCacheData(d->device, MergeCache, &size, nullptr);
  31.         if (res != VK_SUCCESS) {
  32.             NCNN_LOGE("Error getting size of pipeline cache %d\n", res);
  33.         }
  34.         void* data = malloc(size);
  35.         res = vkGetPipelineCacheData(d->device, MergeCache, &size, data);
  36.         if (res != VK_SUCCESS) {
  37.             NCNN_LOGE("Error getting data of pipeline cache %d\n", res);
  38.         }
  39.         fp = fopen(filename, "wb");
  40.         if (!fp) {
  41.             NCNN_LOGE("Failed to open file for writing.\n");
  42.         }
  43.         fwrite(data, size, 1, fp);
  44.         fclose(fp);
  45.         free(data);
  46.     }

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

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

读:

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

  1. #include <vulkan/vulkan.h>
  2. VkPipelineCache GlobalPipelineCache = 0;
  3. unsigned char CacheData[759974] = {xxxxxxxxxxxxxxxx}
  4. void CreateGlobalPipelineCache(VkDevice *device)
  5. {
  6. VkPipelineCacheCreateInfo cacheCreateInfo{};
  7. cacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
  8. cacheCreateInfo.initialDataSize = sizeof(CacheData);
  9. cacheCreateInfo.pInitialData = CacheData;
  10. VkResult result = vkCreatePipelineCache(*device, &cacheCreateInfo, nullptr, &GlobalPipelineCache);
  11. }

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

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

CreateGlobalPipelineCache我把它放在了

  1. VulkanDevice::VulkanDevice(int device_index)
  2.     : 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实现就全部完成了。

初始化速度对比:

方法初始化速度
NCNN8s
NCNN-cache4.6s
MNN-cache3.1s

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/298078
推荐阅读
相关标签
  

闽ICP备14008679号