小芝士|简化OpenCL API梳理说明
OpenCL(Open Computing Language,开放计算语言):从软件视角看,它是用于异构平台编程的框架;从规范视角看,它是异构并行计算的行业标准,由Khronos Group来维护。对于我们实验室的异构程序,同样也需要一套自己的runtime实现,为了兼容国外板卡和我们自己的国产化板卡,选用OpenCL标准去设计实现,在这里记录一些实现过程中的思考。

很多组织会基于OpenCL的规范去维护自己的一套runtime api,比如基于OpenCL3.0的PoCL,我在实现的时候也大多是借鉴这个仓库,他是为了应对CPU和Level Zero GPU目标实现的OpenCL实现,再比如Amd、Intel、Nvdia的硬件平台都有基于OpenCL标准的实现。

我们实验室自己的硬件也需要这样一套基于OpenCL标准的基础api实现。我梳理了一下,需要重点实现的API以及对应功能如下:
基础API说明
对于我们实验室的硬件来说,期望实现的功能主要有:
- 可以面向单platform、单context、多device的异构编程模型;
- 支持单个device拥有多个DDR内存的场景;
- 场景相对单一,因此api设计可以相对简单,不追求完备的OpenCL标准实现,但需要实现满足我们硬件需求的Interface。
API List
为了应对上述场景,需要实现的基础API主要有以下几个:
| API名称 | 功能描述 |
|---|---|
| clGetPlatformIDs | 获取平台ID |
| clGetDeviceIDs | 获取设备ID |
| clCreateContext | 创建上下文 |
| clCreateCommandQueue | 创建命令队列 |
| clCreateBuffer | 创建缓冲区 |
| clEnqueueWriteBuffer | 写入缓冲区 |
| clEnqueueReadBuffer | 读取缓冲区 |
| clEnqueueTask | 任务入队 |
| clSetKernelArg | 设置内核参数 |
| clCreateKernel | 创建内核 |
| clReleaseKernel | 释放内核 |
| clReleaseMemObject | 释放内存对象 |
| clReleaseCommandQueue | 释放命令队列 |
| clReleaseContext | 释放上下文 |
| clReleaseEvent | 释放事件 |
Datastruct List
原有OpenCL的数据结构很复杂,面对我们的应用场景,可以实现以下的数据结构:
1 | |
1 | |
1 | |
1 | |
1 | |
还有一些比较基础的cl数据结构,都是以快速实现功能为目的设计。如果是针对下面这种情况则是完全够用。

具体实现
buffer 绑定流程梳理
对于一个buffer的生命周期,我们看他从创建到绑定这个过程:一个buffer先从clCreateBuffer创建,如果指定了 kernel 和参数索引,会自动调用clSetKernelArg函数完成参数绑定,如果没有显式的指定这个ext flag,那么就需要去单独在程序中调用这个程序。
如何完成参数绑定这个动作
现在我们不需要去设计ext flag,在我们程序中,只需要设计显式调用的流程,那么只需要去考虑如何在clSetKernelArg完成参数绑定这个动作。
clSetKernelArg现在涉及三种不同的情况
参数类型处理 有几种主要的参数类型处理器(xargument的子类):
a) 标量参数(scalar_xargument):
1
2
3
4
5
6
7
8void kernel::scalar_xargument::set(const void* cvalue, size_t sz)
{
if (sz != m_sz)
throw error(CL_INVALID_ARG_SIZE,"Invalid scalar argument size");
m_kernel->set_run_arg_at_index(m_arginfo->index, cvalue, sz);
m_set = true;
}b) 全局内存参数(global_xargument):
1
2
3
4
5
6
7
8
9
10
11
12
13void kernel::global_xargument::set(const void* cvalue, size_t sz)
{
if (sz != sizeof(cl_mem))
throw error(CL_INVALID_ARG_SIZE,"Invalid global_argument size");
auto value = const_cast<void*>(cvalue);
auto mem = value ? *static_cast<cl_mem*>(value) : nullptr;
m_buf = xocl(mem);
if (m_arginfo->index != m_arginfo->no_index)
m_kernel->assign_buffer_to_argidx(m_buf.get(),m_arginfo->index);
m_set = true;
}c) 局部内存参数(local_xargument):
1
2
3
4
5
6
7
8void kernel::local_xargument::set(const void* cvalue, size_t sz)
{
if (cvalue != nullptr)
throw error(CL_INVALID_ARG_VALUE,"Local argument value must be nullptr");
if (sz == 0 || sz > 1024*16)
throw error(CL_INVALID_ARG_SIZE,"Invalid local argument size");
m_set = true;
}核心绑定操作:
- 对于标量: 直接将值复制到kernel参数位置
- 对于全局内存: 建立buffer与kernel参数的关联,并进行内存分配
- 对于局部内存: 仅验证参数合法性
执行层面的参数设置:
1 | |
这个设计的优点:
- 类型安全 - 每种参数类型有专门的处理器
- 错误处理 - 详细的参数验证
- 灵活性 - 支持不同类型的内存和参数
- 性能优化 - 可以进行提前绑定和内存规划
怎么确定参数绑定到哪一个bank
对于每一个全局内存参数,绑定到哪一个bank,是怎么确定的呢?
这个过程通常比较复杂,在xrt库中,有三种方式:
显式的指定
1
2
3// 通过CL_MEM_EXT_PTR_XILINX扩展标志指定bank
cl_mem_ext_ptr_t ext;
ext.flags = bank_id; // 显式指定bank id通过kernel和bank的链接关系去确定
1
2
3
4
5
6
7
8
9
10if (m_memidx>=0) {
// 检查kernel与bank的兼容性
for (auto& karg : m_karg) {
auto kernel = karg.first;
auto argidx = karg.second;
// 验证kernel的计算单元是否可以访问指定bank
if (!kernel->validate_cus(device,argidx,m_memidx))
throw error(...);
}
}自动选择
1
2
3
4
5
6
7
8
9// 获取设备上所有可用的bank bitmap
auto mset = device->get_boh_memidx(boh);
// 优先选择高序号bank(通常group index优先于bank index)
for (int idx=mset.size() - 1; idx >= 0; --idx) {
if (mset.test(idx)) {
m_memidx = idx;
break;
}
}在我们程序中,仅需要第二种和第三种。并且相关数据结构也不需要太复杂,每一个cl_mem需要可控的绑定在1个device的1个ddr中。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!