PoCL Runtime开发需要做的工作主要有以下几个方面:
- 实现UMD API;
- 实现KMD API;
- 制作交叉编译tool chain;
- 编写交叉编译脚本
下面咱们一个一个看。
1 UMD API
开源PoCL代码中有些例子是可以参考的,路径在pocl/lib/CL/devices,有:
- basic:以host CPU作为target device,仅有一个执行单元
- cuda: NV GPU为target device
- pthread:也是以host CPU作为target device,但每个线程作为一个执行单元,可模拟多核执行单元
- 其它:还有很多,不一一列举。
假如,我的target device架构为RISC-V,在pocl/lib/CL/devices路径下添加一个文件夹,参考例子实现即可。我认为在初期端到端流程打通阶段,参考以上三个就可以了,后期根据芯片自定义特性再迭代增加。
下面分几大部分展开讲。
1.1 Runtime 初始化
Runtime API:pocl_xx_init,第一次创建clGetDeviceIDs时调用.
此处及以下,xx代表设备识别名称,如pocl_basic_init
步骤:
1.设置设备初始化信息,包括:设备类型、厂商名称及ID、支持OpenCL版本及相应能力属性、printf_buffer size;
2.通过kmd API获取设备当前属性信息
a.xxGetDevice:获取设备信息;
b.xxDeviceGetName:获取设备名称;
c.xxDeviceGetAttribute:获取设备属性,如:
i.MAX_THREADS_PER_BLOCK
ii.MAX_BLOCK_DIM_X
iii.MAX_BLOCK_DIM_Y
iv.MAX_BLOCK_DIM_Z
d.xxDriverGetVersion:获取当前driver版本信息;
e.xxMemGetInfo:获取当前可用memory大小及memory总大小。
3.初始化device_data;
4.配置builtin kernels,包括OpenCL builtin kernel及GW自定义builtin kernel.
注意:umd高度依赖kmd API,由kmd提供对device的基本操作。
1.2 内存管理
Runtime API:
1.pocl_xx_alloc_mem_obj
2.pocl_xx_submit_read
3.pocl_xx_submit_write
4.pocl_xx_submit_copy
1.3 任务调度
Runtime API:pocl_xx_init_queue,clCreateCommandQueue时调用
步骤:
1.初始化任务队列
a.初始化互斥信号量: queue_data->lock
b.初始化条件信号量:pending_cond和running_cond
c.初始化submit线程
d.初始化finalize线程
队列数据结构定义:
typedef struct pocl_xx_queue_data_s
{
int use_threads;
pthread_t submit_thread;
pthread_t finalize_thread;
pthread_mutex_t lock;
pthread_cond_t pending_cond;
pthread_cond_t running_cond;
_cl_command_node *volatile pending_queue;
_cl_command_node *volatile running_queue;
cl_command_queue queue;
} pocl_xx_queue_data_t;
任务调度模型:
1.4 kernel编译
1.4.1 built-in function
built-in function是device特有的内置函数,通常用来获取device硬件信息和状态查询。
比如读取状态寄存器CSR里core id/cluster id.
路径可与opencl built-in function放在一起,如:pocl/lib/kernel/get_cluster_id.c
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long int uint64_t;
#define M_HARTID_REG 0x0F14 // 定义寄存器地址
uint8_t get_cluster_id()
{
uint64_t mstatus;
// Read the value of CSR defined by the macro M_HARTID_REG
__asm__ volatile("csrr %0, %1" : "=r" (mstatus) : "i" (M_HARTID_REG));
// Extract bits [23:16] from the CSR value
return (mstatus >> 16) & 0xFF;
}
built-in function完成后,需要修改CMakeList构建文件,将built-in function编入库.
// CMakeList路径
pocl/lib/kernel/host/CMakeLists.txt
使用llvm-nm -g xx.bc命令查看确认,bitcode file中已包含get_core_id和get_cluster_id符号。
因为这些built-in function将来运行在riscv架构的device上,因此,需要使用目标结构的编译工具链进行编译。目标结构的编译工具链制作是另外一个话题,咱们单开一讲。
set(CLANG_FLAGS ${HOST_CLANG_FLAGS} ${CLANG_CPUFLAGS}
"--sysroot=/project/gw_toolchain/changyao.fu/usr/riscv-gnu-toolchain-musl/sysroot"
"-ffreestanding" "-march=rv64imafv" "-mabi=lp64f" "-emit-llvm" "-ffp-contract=off")
1.4.2 kernel编译
将自定义算子写成kernel函数,同样需要将kernel编译成riscv架构的binary.
分析basic编译流程,如下:
2 Build program
Compiles an .cl file into LLVM IR.
2.1 From source
2.2 From binary
3 Compile and link
3.1 Compile parallel.bc
pocl_llvm_generate_workgroup_function_nowrite()
Links the input kernel LLVM bitcode and the OpenCL kernel runtime library into one LLVM module.
Output is a LLVM bitcode file that contains a work-group function and its associated launchers.
- copy only the kernel+callgraph from program.bc to parallel.bc.
- run pocl passes
3.2 Compile kernel.so.o
pocl_llvm_codegen()
Run LLVM codegen on input file (parallel-optimized).
Compile into native object file (.so.o).
3.3 Final linking step
Linking kernel.so.o -> kernel.so
以vecadd为例,完成以上步骤后,编译出vecadd.so,为workgroup function.
Generate executable binary
main.c怎么写呢?
for (unsigned long int z = 0; z < pc->num_groups[2]; ++z)
for (unsigned long int y = 0; y < pc->num_groups[1]; ++y)
for (unsigned long int x = 0; x < pc->num_groups[0]; ++x) {
{
_pocl_kernel_vecadd_workgroup ((unsigned char *)arguments, (unsigned char *)pc, x, y, z);
}
}
__asm__ volatile("wfi");