一文說清 OpenCL 框架
背景
-
Read the fucking official documents!
--By 魯迅 -
A picture is worth a thousand words.
--By 高爾基
說明:
-
對不起,我竟然用了一個奪人眼球的標題;
-
我會盡量從一個程序員的角度來闡述
OpenCL
,目標是淺顯易懂,如果沒有達到這個效果,就當我沒說這話; -
子曾經曰過:不懂
Middleware
的系統軟件工程師,不是一個好碼農;
- 介紹 =====
-
OpenCL(Open Computing Language,開放計算語言):從軟件視角看,它是用於異構平臺編程的框架;從規範視角看,它是異構並行計算的行業標準,由 Khronos Group 來維護;
-
異構平臺包括了 CPU、GPU、FPGA、DSP,以及最近幾年流行的各類 AI 加速器等;
-
OpenCL 包含兩部分:
1)用於編寫運行在 OpenCL device 上的kernels
的語言(基於 C99);
2)OpenCL API,至於 Runtime 的實現交由各個廠家,比如 Intel 發佈的opencl_runtime_16.1.2_x64_rh_6.4.0.37.tgz
以人工智能場景爲例來理解一下,假如在某個 AI 芯片上跑人臉識別應用,CPU 擅長控制,AI processor 擅長計算,軟件的 flow 就可以進行拆分,用 CPU 來負責控制視頻流輸入輸出前後處理,AI processor 來完成深度學習模型運算完成識別,這就是一個典型的異構處理場景,如果該 AI 芯片的 SDK 支持 OpenCL,那麼上層的軟件就可以基於 OpenCL 進行開發了。
話不多說,看看 OpenCL 的架構吧。
- OpenCL 架構 ============
OpenCL 架構,可以從平臺模型、內存模型、執行模型、編程模型四個角度來展開。
2.1 Platform Model
平臺模型:硬件拓撲關係的抽象描述
-
平臺模型由一個 Host 連接一個或多個 OpenCL Devices 組成;
-
OpenCL Device,可以劃分成一個或多個計算單元
Compute Unit(CU)
; -
CU 可以進一步劃分成一個或多個處理單元
Processing Unit(PE)
,最終的計算由 PE 來完成; -
OpenCL 應用程序分成兩部分:host 代碼和 device kernel 代碼,其中 Host 運行 host 代碼,並將 kernel 代碼以命令的方式提交到 OpenCL devices,由 OpenCL device 來運行 kernel 代碼;
2.2 Execution Model
執行模型:Host 如何利用 OpenCL Device 的計算資源完成高效的計算處理過程
Context
OpenCL 的 Execution Model 由兩個不同的執行單元定義:1)運行在 OpenCL 設備上的 kernel;2)運行在 Host 上的 Host program;其中,OpenCL 使用 Context 代表 kernel 的執行環境:
Context 包含以下資源:
-
Devices:一個或多個 OpenCL 設備;
-
Kernel Objects:OpenCL Device 的執行函數及相關的參數值,通常定義在 cl 文件中;
-
Program Objects:實現 kernel 的源代碼和可執行程序,每個 program 可以包含多個 kernel;
-
Memory Objects:Host 和 OpenCL 設備可見的變量,kernel 執行時對其進行操作;
NDrange
-
kernel 是 Execution Model 的核心,放置在設備上執行,當 kernel 執行前,需要創建一個索引空間 NDRange(一維 / 二維 / 三維);
-
執行 kernel 實例的稱爲 work-item,work-item 組織成 work-group,work-group 組織成 NDRange,最終將 NDRange 映射到 OpenCL Device 的計算單元上;
有兩種方式來找到 work-item:
-
通過 work-item 的全局索引;
-
先查找到所在 work-group 的索引號,再根據局部索引號確定;
以一維爲例:
-
上圖中總共有四個 work-group,每個 work-group 包含四個 work-item,所以 local_size 的大小爲 4,而 local_id 都是從 0 開始重新計數;
-
global_size 代表總體的大小,也就是 16 個 work-item,而 global_id 則是從 0 開始計數;
以二維爲例:
- 二維的計算方式與一維類似,也是結合 global 和 local 的 size,可以得出 global_id 和 local_id 的大小,細節不表了;
三維的方式也類似,略去。
2.3 Memory Model
內存模型:Host 和 OpenCL Device 怎麼來看待數據
OpenCL 的內存模型中,包含以下幾類類型的內存:
-
Host memory:Host 端的內存,只能由 Host 直接訪問;
-
Global Memory:設備內存,可以由 Host 和 OpenCL Device 訪問,允許 Host 的讀寫操作,也允許 OpenCL Device 中 PE 讀寫,Host 負責該內存中 Buffer 的分配和釋放;
-
Constant Global Memory:設備內存,允許 Host 進行讀寫操作,而設備只能進行讀操作,用於傳輸常量數據;
-
Local Memory:單個 CU 中的本地內存,Host 看不到該區域並無法對其操作,該區域允許內部的 PE 進行讀寫操作,也可以用於 PE 之間的共享,需要注意同步和併發問題;
-
Private Memory:PE 的私有內存,Host 與 PE 之間都無法看到該區域;
2.4 Programming Model
-
在編程模型中,有兩部分代碼需要編寫:一部分是 Host 端,一部分是 OpenCL Device 端;
-
編程過程中,核心是要維護一個 Context,代表了整個 Kernel 執行的環境;
-
從 cl 源代碼中創建 Program 對象並編譯,在運行時創建 Kernel 對象以及內存對象,設置好相關的參數和輸入之後,就可以將 Kernel 送入到隊列中執行,也就是 Launch kernel 的流程;
-
最終等待運算結束,獲取計算結果即可;
- 編程流程 =======
- 上圖爲一個 OpenCL 應用開發涉及的基本過程;
下邊來一個實際的代碼測試跑跑,Talk is cheap, show me the code!
- 示例代碼 =======
-
測試環境:Ubuntu16.04,安裝 Intel CPU OpenCL SDK(
opencl_runtime_16.1.2_x64_rh_6.4.0.37.tgz
); -
爲了簡化流程,示例代碼都不做容錯處理,僅保留關鍵的操作;
-
整個代碼的功能是完成向量的加法操作;
4.1 Host 端程序
#include <iostream>
#include <fstream>
#include <sstream>
#include <CL/cl.h>
const int DATA_SIZE = 10;
int main(void)
{
/* 1. get platform & device information */
cl_uint num_platforms;
cl_platform_id first_platform_id;
clGetPlatformIDs(1, &first_platform_id, &num_platforms);
/* 2. create context */
cl_int err_num;
cl_context context = nullptr;
cl_context_properties context_prop[] = {
CL_CONTEXT_PLATFORM,
(cl_context_properties)first_platform_id,
0
};
context = clCreateContextFromType(context_prop, CL_DEVICE_TYPE_CPU, nullptr, nullptr, &err_num);
/* 3. create command queue */
cl_command_queue command_queue;
cl_device_id *devices;
size_t device_buffer_size = -1;
clGetContextInfo(context, CL_CONTEXT_DEVICES, 0, nullptr, &device_buffer_size);
devices = new cl_device_id[device_buffer_size / sizeof(cl_device_id)];
clGetContextInfo(context, CL_CONTEXT_DEVICES, device_buffer_size, devices, nullptr);
command_queue = clCreateCommandQueueWithProperties(context, devices[0], nullptr, nullptr);
delete [] devices;
/* 4. create program */
std::ifstream kernel_file("vector_add.cl", std::ios::in);
std::ostringstream oss;
oss << kernel_file.rdbuf();
std::string srcStdStr = oss.str();
const char *srcStr = srcStdStr.c_str();
cl_program program;
program = clCreateProgramWithSource(context, 1, (const char **)&srcStr, nullptr, nullptr);
/* 5. build program */
clBuildProgram(program, 0, nullptr, nullptr, nullptr, nullptr);
/* 6. create kernel */
cl_kernel kernel;
kernel = clCreateKernel(program, "vector_add", nullptr);
/* 7. set input data && create memory object */
float output[DATA_SIZE];
float input_x[DATA_SIZE];
float input_y[DATA_SIZE];
for (int i = 0; i < DATA_SIZE; i++) {
input_x[i] = (float)i;
input_y[i] = (float)(2 * i);
}
cl_mem mem_object_x;
cl_mem mem_object_y;
cl_mem mem_object_output;
mem_object_x = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(float) * DATA_SIZE, input_x, nullptr);
mem_object_y = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(float) * DATA_SIZE, input_y, nullptr);
mem_object_output = clCreateBuffer(context, CL_MEM_READ_WRITE, sizeof(float) * DATA_SIZE, nullptr, nullptr);
/* 8. set kernel argument */
clSetKernelArg(kernel, 0, sizeof(cl_mem), &mem_object_x);
clSetKernelArg(kernel, 1, sizeof(cl_mem), &mem_object_y);
clSetKernelArg(kernel, 2, sizeof(cl_mem), &mem_object_output);
/* 9. send kernel to execute */
size_t globalWorkSize[1] = {DATA_SIZE};
size_t localWorkSize[1] = {1};
clEnqueueNDRangeKernel(command_queue, kernel, 1, nullptr, globalWorkSize, localWorkSize, 0, nullptr, nullptr);
/* 10. read data from output */
clEnqueueReadBuffer(command_queue, mem_object_output, CL_TRUE, 0, DATA_SIZE * sizeof(float), output, 0, nullptr, nullptr);
for (int i = 0; i < DATA_SIZE; i++) {
std::cout << output[i] << " ";
}
std::cout << std::endl;
/* 11. clean up */
clRetainMemObject(mem_object_x);
clRetainMemObject(mem_object_y);
clRetainMemObject(mem_object_output);
clReleaseCommandQueue(command_queue);
clReleaseKernel(kernel);
clReleaseProgram(program);
clReleaseContext(context);
return 0;
}
4.2 OpenCL Kernel 函數
- 在 Host 程序中,創建 program 對象時會去讀取 kernel 的源代碼,本示例源代碼位於:
vector_add.cl
文件中
內容如下:
__kernel void vector_add(__global const float *input_x,
__global const float *input_y,
__global float *output)
{
int gid = get_global_id(0);
output[gid] = input_x[gid] + input_y[gid];
}
4.3 輸出
參考
The OpenCL Specification
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/XE-A4jnNJdjH_RyQb0ttzQ