ECE408 Lecture 23 Alternatives to CUDA
这次回顾ECE408 Lecture 23,这次介绍了CUDA的替代品。
课程主页:
搬运视频:
加速计算不再是问题
GPU 供应商包括:
- Nvidia;
- AMD;
- Intel;
- Samsung;
- Apple;
- Qualcomm;
- ARM;
CUDA只是计算加速的一种模型
计算加速的历史:
- OpenGL (1992), DirectX (1995);
- GPGPU(2002);
- CUDA (2007);
- OpenCL (2008);
- OpenACC (2012), C++AMP (2013), RenderScript (2013);
- Metal (2014), SYCL (2014), Vulkan (2016),
ROCm HIP (2016);
MPI、TBB、OpenCV等现有框架适配以提供支持。Caffe、TensorFlow、R、PyCUDA等新框架原生支持加速。
OpenCL、OpenACC、MPI
- OpenCL:开放标准加速API;
- OpenACC:一种“低代码”加速API;
- MPI:大规模、多节点并行API;
加速API的共同特征
硬件:
- 轻量级核心的层次结构;
- 本地暂存器内存;
- 缺乏硬件coherence ;
- 慢速全局原子操作;
- 线程;
软件:
- 面向内核的加速;
- 设备内存与主机内存;
- 软件管理内存;
- Grids, Blocks, Threads;
- 批量同步并行;
OpenCL
- CPU、GPU、DSP、FPGA等的框架(不仅仅是 Nvidia GPU);
- 最初由Apple在 AMD、IBM、Qualcomm、Intel和Nvidia的支持下开发。OpenCL 1.0于2008 年推出;
- OpenCL 2.2 于2017年5月推出;
- Apple宣布在2018年放弃OpenCL;
OpenCL内存模型
OpenCL MatMulti
- 注意与CUDA的相似性;
- WorkGroup类似于Block;
- WorkItem类似于Thread;
__local
类似于__shared
;
代码:
1.// Tiled and coalesced version
2.__kernel void myGEMM2(int M, int N, int K, __global float* A, __global float* B, __global float* C) {
3.
4. // Thread identifiers
5. const int row = get_local_id(0); // Local row ID (max: TS)
6. const int col = get_local_id(1); // Local col ID (max: TS)
7. const int globalRow = TS*get_group_id(0) + row; // Row ID of C (0..M)
8. const int globalCol = TS*get_group_id(1) + col; // Col ID of C (0..N)
9.
10. // Local memory to fit a tile of TS*TS elements of A and B
11. __local float Asub[TS][TS];
12. __local float Bsub[TS][TS];
13.
14. // Initialise the accumulation register
15. float acc = 0.0f;
16. // Loop over all tiles
17. const int numTiles = K/TS;
18. for (int t=0; t<numTiles; t++) {
19.
20. // Load one tile of A and B into local memory
21. const int tiledRow = TS*t + row;
22. const int tiledCol = TS*t + col;
23. Asub[col][row] = A[tiledCol*M + globalRow];
24. Bsub[col][row] = B[globalCol*K + tiledRow];
25.
26. // Synchronise to make sure the tile is loaded
27. barrier(CLK_LOCAL_MEM_FENCE);
28.
29. // Perform the computation for a single tile
30. for (int k=0; k<TS; k++)
31. acc += Asub[k][row] * Bsub[col][k];
32.
33. // Synchronise before loading the next tile
34. barrier(CLK_LOCAL_MEM_FENCE);
35. }
36.
37. // Store the final result in C
38. C[globalCol*M + globalRow] = acc;
39.}
OpenACC
- OpenACC应用程序编程接口(API)提供了一组
- 编译器指令(pragmas);
- 库例程;
- 环境变量;
- 可用于编写在加速器设备(包括GPU和CPU)上运行的数据并行的FORTRAN、C和C++程序;
OpenACC Pragmas
在C和C++ 中,#pragma
指令是向编译器提供标准语言中未指定的信息的方法。
OpenACC抽象机器模型
OpenACC中的简单矩阵-矩阵乘法
1 void computeAcc(float *P, const float *M, const float *N, int Mh, int Mw, int Nw)
2 {
3
4 #pragma acc parallel loop copyin(M[0:Mh*Mw]) copyin(N[0:Nw*Mw]) copyout(P[0:Mh*Nw])
5 for (int i=0; i<Mh; i++) {
6 #pragma acc loop
7 for (int j=0; j<Nw; j++) {
8 float sum = 0;
9 for (int k=0; k<Mw; k++) {
10 float a = M[i*Mw+k];
11 float b = N[k*Nw+j];
12 sum += a*b;
13 }
14 P[i*Nw+j] = sum;
15 }
16 }
17 }
一些观察
- 除了第4行和第6行带有
#pragma
的两行之外,该代码几乎与顺序版本相同 - OpenACC使用编译器指令来扩展基础语言。
- 第4行的
#pragma
告诉编译器为第5行到第16行的“i”循环生成代码,以便循环迭代在加速器上并行执行。 copyin
子句和copyout
子句指定矩阵数据应如何在主机和加速器之间传输。- 第6行的
#pragma
指示编译器将内部“j”循环映射到加速器上的第二级并行性。
- 第4行的
动机
- OpenACC程序员通常可以从编写顺序版本开始,然后使用OpenACC指令注释他们的顺序程序。
- 将生成内核和数据传输的大部分细节留给OpenACC编译器。
- OpenACC代码可以通过忽略编译指示由非OpenACC编译器编译。
经常遇到的问题
- 一些OpenACC pragmas是对OpenACC编译器的提示,它可能会或可能不会采取相应的行动;
- OpenACC的性能在很大程度上取决于编译器的质量;
- 这和CUDA和OpenCL不同;
- 如果忽略pragma,某些OpenACC程序可能会表现不同甚至不正确;
OpenACC设备模型
OpenACC中还有一组最小的API,用于同步、内存管理等。
OpenACC执行模型(术语:Gangs和Workers)
并行与循环构造
代码:
#pragma acc parallel loop copyin(M[0:Mh*Mw]) copyin(N[0:Nw*Mw]) copyout(P[0:Mh*Nw])
for (int i=0; i<Mh; i++) {
…
}
等价于:
#pragma acc parallel copyin(M[0:Mh*Mw]) copyin(N[0:Nw*Mw]) copyout(P[0:Mh*Nw])
{
#pragma acc loop
for (int i=0; i<Mh; i++) {
…
}
}
并行构造
并行构造在加速器上执行;
可以指定gangs数量和每个gang的workers数量;
程序员指令
#pragma acc parallel copyout(a) num_gangs(1024) num_workers(32) { a = 23; }
将创建1024*32个工人。 a=23将由所有1024个gang重复执行;
每个“Gang Loop”做什么?
#pragma acc parallel num_gangs(1024)
{
for (int i=0; i<2048; i++) {
…
}
}
for循环将被1024个gang冗余执行。
#pragma acc parallel num_gangs(1024)
{
#pragma acc loop gang
for (int i=0; i<2048; i++) {
…
}
}
for循环的2048次迭代将分给1024个gang执行。
Worker循环
#pragma acc parallel num_gangs(1024) num_workers(32)
{
#pragma acc loop gang
for (int i=0; i<2048; i++) {
#pragma acc loop worker
for (int j=0; j<512; j++) {
foo(i,j);
}
}
}
将创建1024*32=32K个worker,每个执行1M/32K=32个foo()
实例。
更复杂的例子
#pragma acc parallel num_gangs(32)
{
Statement 1; Statement 2;
#pragma acc loop gang
for (int i=0; i<n; i++) {
Statement 3; Statement 4;
}
Statement 5; Statement 6;
#pragma acc loop gang
for (int i=0; i<m; i++) {
Statement 7; Statement 8;
}
Statement 9;
if (condition)
Statement 10;
}
- Statement 1和2被32个gang重复执行;
- n-for循环迭代分配给32个gang;
抽象基于CUDA的节点
每个节点含有$N$个GPU:
MPI模型
- 许多进程分布在一个集群中;
- 每个进程计算部分输出;
- 进程通过消息传递相互通信(不是全局内存);
- 进程可以通过消息同步;
MPI初始化,信息
- 用户通过在shell中执行来启动有X个进程的MPI作业;
MPIrun –np X
;
int MPI_Init(int *argc, char ***argv)
;- 初始化 MPI;
MPI_COMM_WORLD
;- 由所有分配的节点组成的MPI组;
int MPI_Comm_rank(MPI_Comm comm, int *rank)
;- 通信组中调用进程的rank;
int MPI_Comm_size(MPI_Comm comm, int *size)
;- comm组中的进程数;
向量加法:主进程
int main(int argc, char *argv[]) {
int vector_size = 1024 * 1024 * 1024;
int pid=-1, np=-1;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &pid);
MPI_Comm_size(MPI_COMM_WORLD, &np);
if(np < 3) {
if(0 == pid) printf(“Nedded 3 or more processes.\n");
MPI_Abort( MPI_COMM_WORLD, 1 ); return 1;
}
if(pid < np - 1)
compute_node(vector_size / (np - 1));
else
data_server(vector_size);
MPI_Finalize();
return 0;
}
MPI发送数据
int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
;Buf
:发送缓冲区的起始地址;Count
:发送缓冲区中的元素数(非负整数);Datatype
:每个发送缓冲区元素的数据类型;Dest
:目的地rank(整数);Tag
:消息标签(整数);Comm
:通讯器(句柄)
MPI接收数据
int MPI_Recv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status)
;Buf
:接收缓冲区的起始地址;Count
:接收缓冲区中的最大元素数(非负整数);Datatype
:每个接收缓冲区元素的数据类型;Source
:来源rank(整数);Tag
:消息标签(整数);Comm
:通讯器(句柄);Status
:状态对象;
向量加法:服务器进程
void data_server(unsigned int vector_size) {
int np, num_nodes = np – 1, first_node = 0, last_node = np - 2;
unsigned int num_bytes = vector_size * sizeof(float);
float *input_a = 0, *input_b = 0, *output = 0;
/* Set MPI Communication Size */
MPI_Comm_size(MPI_COMM_WORLD, &np);
/* Allocate input data */
input_a = (float *)malloc(num_bytes);
input_b = (float *)malloc(num_bytes);
output = (float *)malloc(num_bytes);
if(input_a == NULL || input_b == NULL || output == NULL) {
printf("Server couldn't allocate memory\n");
MPI_Abort( MPI_COMM_WORLD, 1 );
}
/* Initialize input data */
random_data(input_a, vector_size, 1, 10);
random_data(input_b, vector_size, 1, 10);
/* Send data to compute nodes */
float *ptr_a = input_a;
float *ptr_b = input_b;
for(int process = 1; process < last_node; process++) {
MPI_Send(ptr_a, vector_size / num_nodes, MPI_FLOAT,
process, DATA_DISTRIBUTE, MPI_COMM_WORLD);
ptr_a += vector_size / num_nodes;
MPI_Send(ptr_b, vector_size / num_nodes, MPI_FLOAT,
process, DATA_DISTRIBUTE, MPI_COMM_WORLD);
ptr_b += vector_size / num_nodes;
}
/* Wait for compute to complete*/
MPI_Barrier(MPI_COMM_WORLD);
/* Collect output data */
MPI_Status status;
for(int process = 0; process < num_nodes; process++) {
MPI_Recv(output + process * num_points / num_nodes,
num_points / num_comp_nodes, MPI_REAL, process,
DATA_COLLECT, MPI_COMM_WORLD, &status );
}
/* Store output data */
store_output(output, dimx, dimy, dimz);
/* Release resources */
free(input);
free(output);
}
向量加法:计算进程
void compute_node(unsigned int vector_size ) {
int np;
unsigned int num_bytes = vector_size * sizeof(float);
float *input_a, *input_b, *output;
MPI_Status status;
MPI_Comm_size(MPI_COMM_WORLD, &np);
int server_process = np - 1;
/* Alloc host memory */
input_a = (float *)malloc(num_bytes);
input_b = (float *)malloc(num_bytes);
output = (float *)malloc(num_bytes);
/* Get the input data from server process */
MPI_Recv(input_a, vector_size, MPI_FLOAT, server_process,
DATA_DISTRIBUTE, MPI_COMM_WORLD, &status);
MPI_Recv(input_b, vector_size, MPI_FLOAT, server_process,
DATA_DISTRIBUTE, MPI_COMM_WORLD, &status);
/* Compute the partial vector addition */
for(int i = 0; i < vector_size; ++i) {
output[i] = input_a[i] + input_b[i];
}
/* Or, can offload to GPU here */
/* cudaMalloc(), cudaMemcpy(), kernel launch, etc. */
MPI_Barrier(MPI_COMM_WORLD);
/* Send the output */
MPI_Send(output, vector_size, MPI_FLOAT,
server_process, DATA_COLLECT, MPI_COMM_WORLD);
/* Release memory */
free(input_a);
free(input_b);
free(output);
}
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Doraemonzzz!
评论
ValineLivere