前阵子遇到了个日了狗的奇怪问题。安卓跑在Adreno GPU上的OpenCL程序显存实际占用是程序分配大小的两倍。并且前前后后在多个平台上测试过,得到的信息如下:
- Intel OpenCL正常,7700K CPU
- nVIDIA OpenCL正常,GTX1060 GPU
- oppo r17 pro异常,显存翻倍,Snapdragon 710/Adreno 616
- vivo iqoo pro异常,显存翻倍, Snapdragon 855+/Adreno 640
- huawei mate30 pro正常,Kirin 990/Mali-G76
已有证据表明似乎只有Adreno的OpenCL实现上存在这个恶心问题。更具体地,在分配buffer和image时,只要读写权限不是CL_MEM_READ_ONLY的,都会观测到显存占用翻倍。初步推测是驱动实现的问题,但安卓平台似乎也没办法绕过厂商更改驱动。
这个问题最终通过在Adreno GPU上避开OpenCL默认的显存分配机制得到了解决:使用高通提供的cl_qcom_ion_host_ptr扩展,基于ION buffer分配存储。但由于历史原因,内外网能找到的案例没有一个能在较新Android上完整跑通的(日了狗了x2)。最后还是参考了Android 8.0的libion的实现源码,总结出了OpenCL的ION内存从分配到映射到释放的一条龙流程。
首先需要添加以下的头文件引用,除了CL相关的外,其它都是ION buffer相关的:
#include <CL/cl.h>
#include <CL/cl_ext.h>
#include <linux/ion.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
在使用cl_qcom_ion_host_ptr扩展时,最好确认设备支持这个扩展,以免再非高通设备或者高通老旧设备上出现问题。
size_t extension_length = 0;
clGetDeviceInfo(device_, CL_DEVICE_EXTENSIONS, 0, nullptr, &extension_length);
std::vector<char> extension(extension_length);
clGetDeviceInfo(device_, CL_DEVICE_EXTENSIONS, extension_length, extension.data(), nullptr);
// 检查extension中是否包含cl_qcom_ion_host_ptr
第一步,用户态的ION buffer需要通过打开和读取/dev/ion文件,获得第一个fd。这个fd可以理解为进程对ION的操作句柄,一个进程只需要一个,每个ION操作都需要提供它:
auto fd = open("/dev/ion", O_RDONLY);
正常情况下,fd如果为-1表面这一步出错,后续操作也不会成功。
第二步,通过上面的fd分配ION内存。注意,Adreno OpenCL通常需要ION内存分配提供一个额外的padding空间,同时对齐到指定边界上。我们需要事先向设备查询这两个数值。
size_t device_page_size = 0;
size_t ext_mem_padding_in_bytes = 0;
clGetDeviceInfo(device_, CL_DEVICE_PAGE_SIZE_QCOM, sizeof(ext_mem_padding_in_bytes), &ext_mem_padding_in_bytes, nullptr);
clGetDeviceInfo(device_, CL_DEVICE_EXT_MEM_PADDING_IN_BYTES_QCOM, sizeof(device_page_size), &device_page_size, nullptr);
ion_allocation_data allocation_data = { 0 };
allocation_data.len = size + ext_mem_padding_in_bytes;
allocation_data.align = device_page_size;
allocation_data.flags = ION_FLAG_CACHED;
// allocation_data.flags = 0;
allocation_data.heap_id_mask = -1;
ioctl(fd, ION_IOC_ALLOC, &allocation_data);
// 分配结果储存在 allocation_data.handle
这里需要注意三点。
- 这里的allocation_data.flags的区别一般在于是否启用ION_FLAG_CACHED(启用通常可以带来显著的读写性能提升)。注意这里的选择将影响后续clCreateBuffer/clCreateImage时的另一个flag。
- heap_id_mask指示了可以在哪些堆上分配这块buffer。-1即二进制全1允许在任何堆上分配,这也是libion测试程序的做法。通常堆会有一个分配的优先级,一般来说实际选择的堆也是最常见的堆,这对于OpenCL来说足够使用了。如果不使用-1,heap_id_mask也可以手动选择,但不同系统版本上的heap mask宏定义通常不一样。这是非常操蛋一点。
- ioctl返回值或allocation_data.handle如果为-1的话,表面这一步出错,后续操作也不会成功。
第三步,共享这块ION buffer,获得第二个fd。这个fd用于在不同进程/设备上共享这块ION buffer,需要和第一个fd区分开:
ion_fd_data fd_data = { 0 };
fd_data.handle = allocation_data.handle;
ioctl(fd, ION_IOC_SHARE, &fd_data)
// 共享结果储存在 fd_data.fd
同样,如果这里ioctl返回值或fd_data.fd为-1,表面这一步出错,后续操作也不会成功。
第四步,映射这块ION buffer到当前进程。
void * data = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_data.fd, 0);
如果data为nullptr,表面这一步出错,后续操作也不会成功。
第五步,根据ION buffer分配OpenCL存储对象,用到第三步的fd_data.fd和第四步的data。这里以cl_buffer为例:
cl_mem_ion_host_ptr ion_host_ptr = { 0 };
ion_host_ptr.ext_host_ptr.allocation_type = CL_MEM_ION_HOST_PTR_QCOM;
ion_host_ptr.ext_host_ptr.host_cache_policy = CL_MEM_HOST_WRITEBACK_QCOM;
// ion_host_ptr.ext_host_ptr.host_cache_policy = CL_MEM_HOST_IOCOHERENT_QCOM;
// ion_host_ptr.ext_host_ptr.host_cache_policy = CL_MEM_HOST_UNCACHED_QCOM;
ion_host_ptr.ion_filedesc = fd_data.fd;
ion_host_ptr.ion_hostptr = data;
cl_int err;
cl_mem buffer = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR | CL_MEM_EXT_HOST_PTR_QCOM, size, &ion_host_ptr, &err); // context 怎么来的应该不需要多说吧
注意这里的host_cache_policy有三个取值,需要与第二步一致,否则将导致不可预期的结果。具体地:
- 如果allocation_data.flags没有ION_FLAG_CACHED,此处必须要CL_MEM_HOST_UNCACHED_QCOM。
- 如果allocation_data.flags有ION_FLAG_CACHED,且设备支持cl_mem_ext_host_ptr_iocoherent扩展,则可以为CL_MEM_HOST_WRITEBACK_QCOM,也可以为CL_MEM_HOST_IOCOHERENT_QCOM。
- 如果allocation_data.flags有ION_FLAG_CACHED,且设备不支持cl_mem_ext_host_ptr_iocoherent扩展,则只能为CL_MEM_HOST_WRITEBACK_QCOM。
接下来可以正常地使用buffer。
在资源释放方面,没有找到特别的文档说明上述各个步骤的释放顺序,不过一般来说,遵循后构造先释放的原则不会产生太大的问题。
第六步,释放第五步中的buffer:
clReleaseMemObject(buffer);
第七步,munmap第四步中的data:
munmap(data, size);
这里munmap的返回值不应该为-1,否则代表出错。
第八步,释放第三步中的fd_data.fd:
close(fd_data.fd);
这里close的返回值不应该为-1,否则代表出错。
第九步,释放第二步中的fd_data.handle:
ion_handle_data handle_data = { 0 };
handle_data.handle = fd_data.handle;
ioctl(fd, ION_IOC_FREE, &handle_data);
这里ioctl的返回值不应该为-1,否则代表出错。
第十步,释放第一步中的fd:
close(fd);
这里close的返回值不应该为-1,否则代表出错。