OPENGL拿到的相机帧,通过有拷贝的方式进行GPU推理CPU占用率太高,而NCNN没有提供OPENGL零拷贝GPU推理的接口,因此只能自己实现
主要方法是使用Android Hardware Buffer 实现纹理的共享,在OPENGL上对相机数据进行预处理后,将纹理信息写入到Android Hardware Buffer,随后在vulkan上进行转格式,最后使用NCNN的GPU推理,实现GPU的零拷贝。
非官方实现,主要是多了一步RGBA转RGB的操作,会有几个ms的开销,如果从零开始的话,建议尝试其他自带OPENGL零拷贝接口的推理框架。
首先在opengl初始化的时候创建Android Hardware Buffer,这边创建的是一块320*256大小的RGBA unsigned char内存块。将buffer绑定到EGLimage上,再将EGLimage与opengl的纹理进行绑定。
AHardwareBuffer_Desc desc = {
.width = 320,
.height = 256,
.layers = 1,
.format = AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT,
.usage = AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT| AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER | AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE ,
};
int ret = AHardwareBuffer_allocate(&desc, &buffer);
if (ret != 0) {
ALOGE("AHardwareBuffer_allocate error");
}
//通过EGL与opengl纹理进行绑定
EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID(buffer);
if (!clientBuffer) {
ALOGE("clientBuffer error");
}
if (EglImage != EGL_NO_IMAGE_KHR) {
eglDestroyImageKHR(display, EglImage);
}
EGLint eglImageAttributes[] = { EGL_NONE };
EglImage = eglCreateImageKHR(display, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID,
clientBuffer, eglImageAttributes);
if (EglImage == EGL_NO_IMAGE_KHR) {
ALOGE("EglImage error");
}
//建立faceDetectAlignFramerbuffer
faceDetectAlignFramerbuffer = 0;
glGenFramebuffers(1, &faceDetectAlignFramerbuffer);
glGenTextures(1, &faceDetectAlignTexture);
glBindFramebuffer(GL_FRAMEBUFFER, faceDetectAlignFramerbuffer);
glBindTexture(GL_TEXTURE_2D, faceDetectAlignTexture);
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)EglImage);
usage这里的标志位自测没什么影响,改成一个或者CPU那些标志位也不影响使用,时间上或许有影响,没测过。
绑定完后就可以按照FBO的流程对这块纹理进行绘制,绘制完毕后数据就被写入到Android Hardware Buffer上了。注意在glDrawElements后要加上glFinish(),否则Android Hardware Buffer会没有数据(正常渲染是不需要加glFinish的,但是渲染到Android Hardware Buffer就必须要加,具体原因我也不清楚,了解的朋友可以告诉我)。
此时,Android Hardware Buffer就已经有了相机的数据了,那么接下来就是把Android Hardware Buffer绑定到VkImageMat上。
头文件定义:
ncnn::Option opt;
ncnn::VulkanDevice* g_vkdev = 0;
ncnn::VkAllocator* g_blob_vkallocator = 0;
ncnn::VkAllocator* g_staging_vkallocator = 0;
ncnn::UnlockedPoolAllocator g_blob_pool_allocator;
ncnn::PoolAllocator g_workspace_pool_allocator;
ncnn::VkImageMat VkImageMatOut;
ncnn::Mat MatOut;
ncnn::VkImageMat VkImageMatIn;
ncnn::VkImageMat VkImageMatOrigin;
ncnn::VkCompute *vkcompute;
ncnn::ImportAndroidHardwareBufferPipeline *import_pipeline;
ncnn::VkAndroidHardwareBufferImageAllocator* ahb_im_allocator;
初始化函数:
int initParam(AHardwareBuffer* buffer, int w, int h, int c)
{
g_vkdev = ncnn::get_gpu_device(0);
g_blob_vkallocator = new ncnn::VkBlobAllocator(g_vkdev);
g_staging_vkallocator = new ncnn::VkStagingAllocator(g_vkdev);
opt.blob_vkallocator = g_blob_vkallocator;
opt.workspace_vkallocator = g_blob_vkallocator;
opt.staging_vkallocator = g_staging_vkallocator;
g_blob_vkallocator->clear();
g_staging_vkallocator->clear();
opt.lightmode = true;
opt.num_threads = 1;
g_blob_pool_allocator.set_size_compare_ratio(0.0f);
g_workspace_pool_allocator.set_size_compare_ratio(0.5f);
opt.blob_allocator = &g_blob_pool_allocator;
opt.workspace_allocator = &g_workspace_pool_allocator;
opt.use_winograd_convolution = true;
opt.use_sgemm_convolution = true;
opt.use_int8_inference = true;
opt.use_vulkan_compute = true;
opt.use_fp16_packed = true;
opt.use_fp16_storage = true;
opt.use_fp16_arithmetic = true;
opt.use_int8_storage = true;
opt.use_int8_arithmetic = true;
opt.use_packing_layout = false;
opt.use_shader_pack8 = false;
opt.use_image_storage = true;
g_blob_pool_allocator.clear();
g_workspace_pool_allocator.clear();
pNet->opt = opt;
pNet->set_vulkan_device(g_vkdev);
import_pipeline = new ncnn::ImportAndroidHardwareBufferPipeline(g_vkdev);
vkcompute = new ncnn::VkCompute(g_vkdev);
ahb_im_allocator = new ncnn::VkAndroidHardwareBufferImageAllocator(g_vkdev, buffer);
import_pipeline->create(ahb_im_allocator, 1, 1, w, h, opt);
VkImageMatOrigin.create(w, h, c, sizeof(float16_t), ahb_im_allocator);
VkImageMatIn.create(w, h, 3, sizeof(float16_t), g_blob_vkallocator);
}
w,h,c代表Android Hardware Buffer的width,height和channel,这里分别为320,256,4。
执行函数:
ncnn::Extractor ex = pNet->create_extractor();
ex.set_blob_vkallocator(g_blob_vkallocator);
ex.set_workspace_vkallocator(g_blob_vkallocator);
ex.set_staging_vkallocator(g_staging_vkallocator);
vkcompute->record_import_android_hardware_buffer(import_pipeline, VkImageMatOrigin, VkImageMatIn);
ex.input(FirstNodeNam, VkImageMatIn);
ex.extract(EndNodeName, VkImageMatOut, *vkcompute);
vkcompute->record_download(VkImageMatOut, MatOut, opt);
vkcompute->submit_and_wait();
vkcompute->reset();
最后修改NCNN convert_ycbcr.comp文件,把
vec3 rgb = texture(android_hardware_buffer_image, pos).rgb * 255.f;
改成
vec3 rgb = texture(android_hardware_buffer_image, pos).rgb;
修改完毕后再重新编译,这样就可以成功运行NCNN的GPU零拷贝了。
以下是在8155下运行320*256大小depth_multiple: 0.2,width_multiple: 0.15 的yolov5n的对比:
单人脸检测速度 | CPU使用率 | GPU使用率 | |
MNN GPU有拷贝 | 30ms | 52% | 17% |
NCNN GPU零拷贝 | 25ms | 11% | 24% |
检测速度指的是整个线程执行一帧的时间,包含opengl的渲染、数据的拷贝以及后处理。
CPU使用率指单核的使用率。
因芯片性能、运行环境不同,性能对比仅供参考。
我是在ncnn 20231027版本上实现的,其他版本没有测试过。
如果Android Hardware Buffer可以创建R16G16B16或者R32G32B32的数据,就不需要上述这些操作直接进行NCNN GPU推理了,但是我尝试下来Android Hardware Buffer并不支持。就算创建R16G16B16A16内存块,且OPENGL输出R16G16B16,最后得到的也是R16G16B16A16的数据。(有了解的朋友可以告诉我)
后来参考这里的代码cam-ncnn-win/app/src/main/jni/main_activity_jni.cpp at 0412b3767c0e65f7b81379fa7a73be459788baf9 · yyangoO/cam-ncnn-win (github.com)
在他的注释里面有数据的转换record_import_android_hardware_buffer,不过他转的是yuv420,一开始看代码的时候,ImportAndroidHardwareBufferPipeline的create_sampler函数有用到VkSamplerYcbcrConversionInfoKHR结构体。
GPT对它的描述:用于将纹理数据从 Y'CbCr 格式转换为 RGB 格式的过程中使用的采样器转换对象。它是使用 Vulkan 中的 VK_KHR_sampler_ycbcr_conversion 扩展实现的。
以为RGBA转不了,后来试了一下,这个VkSamplerYcbcrConversionInfoKHR也可以转RGBA,所以就打算用record_import_android_hardware_buffer进行RGBA到RGB的转换。
花了几天尝试后,发现down的图没有进行归一化,只能继续看NCNN的代码,找到record_import_android_hardware_buffer的vulkan着色器文件convert_ycbcr.comp,发现它在绘制的时候乘了255.f,把这段去掉就可以得到归一化的图了。
另外
import_pipeline->create(ahb_im_allocator, 1, 1, w, h, opt);
这个函数的实现在pipeline.cpp里,第二第三个参数代表着色器代码里的type_to和rotate_from,因为我需要将RGBA转为RGB,所以第二个参数为1,我不需要旋转,所以第三个参数为1。
目前尝试下来AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT和AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM都可以实现,AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420没试过但应该也是可行的,其他的16、32数据类型都试过不可行,在绑定到opengl纹理的时候就报错了。
最后,VkImageMat的定义一定要放到初始化函数里,如果放到执行函数里,它会造成函数内外执行时间的不一致,也就是在函数内部测出来时间没增加,但是在函数外部测出来时间增加了,我用过两种计时工具都测出来时间不一致,具体原因我也不是很清楚,了解的朋友可以告诉我一下。
因项目需要实现GPU零拷贝,从开始编译NCNN到最后实现GPU零拷贝,总共花了2个多月时间。感谢nihui和NCNN交流群的各位朋友们帮忙答疑,如果nihui当初不说logcat 搜索 ncnn,logcat显示no vulkan device我甚至不知道设备没有开启vulkan,也就没有后面的实现了。
在这里也抛砖引玉,分享NCNN 的OPENGL GPU零拷贝的实现,如果大家有更好的实现方式,可以一起交流。