矩阵与向量乘法的 cuda 优化

28
矩矩矩矩矩矩矩矩 CUDA 矩矩 矩矩 2010 矩 12 矩 11 矩 2011 矩 1 矩 8 矩矩矩 1

Upload: arin

Post on 05-Jan-2016

296 views

Category:

Documents


0 download

DESCRIPTION

矩阵与向量乘法的 CUDA 优化. 风辰 2010 年 12 月 11 日 2011 年 1 月 8 日修订. 目的. 对于 CUDA 程序开发来说,优化往往是整个开发过程的核心,不同算法,不同存储器组织的程序性能往往差几十倍,本文通过一个简单的例子来展示 CUDA 开发中一些重要的因素对性能的影响。. 假设读者拥有以下知识. 拥有 C 语言编程的经验 , 最好拥有并行编程经验 懂得 CUDA, 最好用 CUDA 写过代码. 测试环境. Intel xeon 5405 2.0 GHz Geforce GTX 295( 只使用单核 ) Gcc 4.3.3 - PowerPoint PPT Presentation

TRANSCRIPT

Page 1: 矩阵与向量乘法的 CUDA 优化

矩阵与向量乘法的 CUDA优化风辰

2010 年 12 月 11日2011 年 1 月 8 日修订

1

Page 2: 矩阵与向量乘法的 CUDA 优化

目的• 对于 CUDA程序开发来说,优化往往是整个开发过程的核心,不同算法,不同存储器组织的程序性能往往差几十倍,本文通过一个简单的例子来展示 CUDA开发中一些重要的因素对性能的影响。

2

Page 3: 矩阵与向量乘法的 CUDA 优化

假设读者拥有以下知识拥有 C语言编程的经验 ,最好拥有并行编程经验

懂得 CUDA,最好用 CUDA写过代码

3

Page 4: 矩阵与向量乘法的 CUDA 优化

测试环境Intel xeon 5405 2.0 GHz

Geforce GTX 295(只使用单核 )

Gcc 4.3.3

CUDA toolkit 3.1

只测试计算时间 ,不包括数据传输

4

Page 5: 矩阵与向量乘法的 CUDA 优化

符号说明• matrix:矩阵数据指针 ,以行为主序或者列为主序存储

• v || vec: 向量指针• r: 矩阵和向量乘的结果指针• rowSize: 表示矩阵的行数 ,也是 r的长度• columnSize:表示矩阵的列数 ,也是 v的长度

• 所有指向显存的指针加前缀 d_5

Page 6: 矩阵与向量乘法的 CUDA 优化

编译配置矩阵尺寸 8192*8192 单精度编译选项 -O3 –funroll-loops –msse

CPU计时函数采用 gettimeofday, clock,GPU计时函数采用 CUDA event

6

Page 7: 矩阵与向量乘法的 CUDA 优化

串行 C版本算法 :遍历矩阵行 ,每行和向量相乘 ,最终结果为一向量

void mxv(const int rowSize, const int columnSize, const float *matrix, const float *v, float *r){

for(int i = 0; i < rowSize; i++){ float re = 0.0f; for(int j = 0; j < columnSize; j++){ re += matrix[i*columnSize+j]*v[j]; } r[i] = re; }} 7运行时间 120 ms,不使用 -O3运行耗时

490 ms

Page 8: 矩阵与向量乘法的 CUDA 优化

简单 SSE版本算法: 利用 sse指令计算矩阵每行和向量的乘积void mxvSSE(const int rowSize, const int columnSize, const float *matrix,

const float *v, float *r){ __m128 *mv = (__m128*)v; __m128 *mm = (__m128*)matrix; for(int i = 0; i < rowSize; i++){ __m128 re = _mm_set_ps(0.0f, 0.0f, 0.0f, 0.0f); for(int j = 0; j < columnSize/4; j++){ re = _mm_add_ps(re, _mm_mul_ps(mm[i*columnSize/4+j], mv[j])); }

float __attribute((aligned(16))) a[4]; _mm_store_ps(a, re); r[i] = a[0] + a[1] + a[2] + a[3]; }}

运行时间 99ms 8

Page 9: 矩阵与向量乘法的 CUDA 优化

SSE + openmp

算法: 使用二线程并行计算行循环void mxvSSEOpenmp(const int rowSize, const int columnSize, float *matrix,

float *vec, float *r){__m128 *mv = (__m128*)v;

__m128 *mm = (__m128*)matrix;#pragma omp parallel for num_threads(2)

for(int i = 0; i < rowSize; i++){ __m128 re = _mm_set_ps(0.0f, 0.0f, 0.0f, 0.0f); for(int j = 0; j < columnSize/4; j++){ re = _mm_add_ps(re, _mm_mul_ps(mm[i*columnSize/4+j], mv[j])); } float __attribute((aligned(16))) a[4]; _mm_store_ps(a, re); r[i] = a[0] + a[1] + a[2] + a[3];}

运行时间 50ms9

Page 10: 矩阵与向量乘法的 CUDA 优化

CUDA优化注意事项一、选择好的并行方式选择好的算法,以发掘更多的数据并行性

二、保持 SM忙碌尽量利用所有的 SM参与计算,可以通过加大数据量或减小线程块大小达到目的

三、优化存储器利用保证全局存储器合并访问使用速度更快的 constant 或 shared存储器

10

Page 11: 矩阵与向量乘法的 CUDA 优化

CUDA-naïve版本算法 : 每个 CUDA线程计算矩阵的一行与向量乘积static void __global__ mxvNaive(int rowSize, int columnSize, int

columnPitch, const float *d_matrix, const float *d_vec, float *d_r){

uint id = blockDim.x*blockIdx.x + threadIdx.x; if(rowSize <= id) return; float temp = 0.0f;

#pragma unroll 4 for(int i = 0; i < columnSize; i++){

temp += d_matrix[id*columnPitch+i]*d_vec[i]; } d_r[id] = temp;}

耗时 150 ms > 串行 120ms 11

Page 12: 矩阵与向量乘法的 CUDA 优化

CUDA-naïve为什么比串行还慢? columnPitch的作用是什么?访问 d_matrix没有满足合并访问的要求

什么是合并访问?

12

Page 13: 矩阵与向量乘法的 CUDA 优化

合并访问一句话:相邻线程访问段对齐的相邻地址为什么说访问 d_matrix没有满足合并访问要求

for(int i = 0; i < columnSize; i++){ temp += d_matrix[id*columnPitch+i]*d_vec[i]; }假设 i=0, 线程 0 访问 d_matrix[0],线程 1 访问

d_matrix[columnPitch],线程 2 访问 d_matrix[2*columnPitch],这些数据的地址并不相邻,因此没有满足合并访问的要求。

columnPitch由函数 cudaMallocPitch返回,保证段对齐。

怎样才能使用访问 d_matrix满足合并访问要求?

13

Page 14: 矩阵与向量乘法的 CUDA 优化

矩阵转置转置后访问 d_matrix的模式变成了for(int i = 0; i < rowSize; i++){

temp += d_matrix[i*columnPitch+id]*d_vec[i];

}

  假设 i=0, 线程 0 访问 d_matrix[0],线程 1 访问d_matrix[1 ],线程 2 访问 d_matrix[2],此时满足合并访问的要求。

  此时运行时间下降到了 4.65ms, 性能提高到原来的 30多倍 , 这充分说明了合并访问的重要性。

14

Page 15: 矩阵与向量乘法的 CUDA 优化

更进一步for(int i = 0; i < rowSize; i++){

temp += d_matrix[i*columnPitch+id]*d_vec[i];

}从上面代码很明显的看到 d_vec在计算的过程中不变,而且每个线程都访问相同的地址 , 故可以考虑将它存放在 constant中

15

Page 16: 矩阵与向量乘法的 CUDA 优化

constant优化static void __global__ mxvNaiveTransposeConstant(int rowSize, int columnSize, int

columnPitch, const float *d_matrix, const int start, float *d_r){ uint id = blockDim.x*blockIdx.x + threadIdx.x; if(columnSize <= id) return; float temp = 0.0f; int end = start+CONSTANTSIZE > rowSize ? rowSize : start+CONSTANTSIZE; for(int i = start; i < end; i++){ temp += d_matrix[i*columnPitch+id]*c_v[i-start];} d_r[id] += temp;}

其中 : c_v 中 constant存储器数组 , 大小为CONSTANTSIZE。

16

耗时 4.17 ms

Page 17: 矩阵与向量乘法的 CUDA 优化

constant优化 ( 续 )

问题:如果 d_v的大小超过 constant 的 64KB大小限制,怎么办?

解决方法:分批,多次传输和启动内核

17

Page 18: 矩阵与向量乘法的 CUDA 优化

更进一步很明显 , 对于 block内线程来说 ,向量都是共享的 ,因此我们可以使用比 constant更快的 shared memory来存储 ,此时相比使用 constant,我们免掉了在向量比较大时多次数据拷贝和启动 kernel的开销 ,而且没有使用全局变量 ,代码的可扩展性更好 .

由于可能因为 shared memory大小存储不了向量 ,因此需要将向量分块 ,每次传一小块到 shared 中 ,计算完这一小块后 ,再传一小块接着计算 .

18

Page 19: 矩阵与向量乘法的 CUDA 优化

shared优化static void __global__ mxvNaiveTransposeShared(int rowSize, int columnSize, int columnPitch,

const float *d_matrix, const float *d_v, const int sharedSize, float *d_r){

uint id = blockDim.x*blockIdx.x + threadIdx.x;

float temp = 0.0f;

extern __shared__ float s_v[];

for(int start = 0; start < rowSize; start += sharedSize){

__syncthreads();

#pragma unroll 4

for(int i = threadIdx.x; i < sharedSize&&i+start<rowSize; i += blockDim.x){

s_v[i] = d_v[start+i];

} __syncthreads();

if(columnSize <= id) continue;

int end = start+sharedSize > rowSize ? rowSize : start+sharedSize;

19

Page 20: 矩阵与向量乘法的 CUDA 优化

shared优化 ( 续 )#pragma unroll 8

for(int i = start; i < end; i++){

temp += d_matrix[i*columnPitch+id]*s_v[i-start];

}

}

if(id < columnSize)

d_r[id] = temp;

}

20耗时 2.62 ms

Page 21: 矩阵与向量乘法的 CUDA 优化

矩阵转置的性能前面的 CUDA代码都是基于转置后的矩阵来计算的 ,因此矩阵转置的性能非常重要 ,下面的 sdk中的 transposeNew转置 8192*8192 的 float在 GTX 295上的数据

21

方法说明 吞吐量 Kernel运行时间transposeNew-Outer-fine-grained 67.7686 GB/s 7.37804 s

transposeNew-Inner-fine-grained 72.7973 GB/s 6.86839 s

transposeNew-Outer-diagonal transpose 28.4115 GB/s 17.59853 s

transposeNew-Inner-diagonal transpose 33.8458 GB/s 14.77287 s

transposeNew-Outer-no bank conflict trans 17.2629 GB/s 28.96379 s

transposeNew-Inner-no bank conflict trans s17.0058 GB/s 29.40170

由于矩阵转置比较慢,因此在很多情况下,我们要使用不转置矩阵的办法

Page 22: 矩阵与向量乘法的 CUDA 优化

关于 block 和 warpBlock , CUDA线程以 block为单位分发到 SM上执行,因此使用block线程为单位来处理数据是一个很 nature的选择。

Warp , block中的线程会以 32个为单位划分,这 32个线程称为warp, warp中线程的 id是连续的,由于 SM调度线程的单位是warp,因此在某些情况下,显式的使用 warp可获得更好的性能。

22

Page 23: 矩阵与向量乘法的 CUDA 优化

Block模式算法 :一个 block处理矩阵的一行和向量乘积,其中 block中的每个线程处理该行中的一个与对应向量元素的乘积 ,然后归约。

static void __global__ mxvBlock(int rowSize, int columnSize, int pitchItem, const float* __restrict__ d_matrix,const float* __restrict__ d_vec, float* __restrict__ d_r){unsigned int tid = threadIdx.x;extern __shared__ float s_r[];float temp = 0.0f;for(int i = tid; i < columnSize; i += blockDim.x){

temp += d_matrix[blockIdx.x*pitchItem+i]*d_vec[i];}s_r[tid] = temp; __syncthreads();……//省略归约代码}

}

23耗时 5.42 ms

Page 24: 矩阵与向量乘法的 CUDA 优化

Warp模式

耗时 4.10 ms

24

具体的计算和 block模式差不多 , 只是使用一个 warp线程计算矩阵的一行与向量的乘积 , 在我的测试中发现 , 这个算法对于行大于列的矩阵效果很好 ,很多时候性能是 block的两倍以上。

Page 25: 矩阵与向量乘法的 CUDA 优化

cublas

函数 : cublasSgemv

25

耗时 2.61 ms

Page 26: 矩阵与向量乘法的 CUDA 优化

总结一下函数名 说明 时间 /ms 加速比mxv 串行 C 120 1mxvSSE 串行 C+SSE 99 1.2

mxvSSEOpenmp 串行 C+SSE+openmp 50 2.4

mxvNaive 150 0.8

mxvNaiveTranspose 矩阵转置 4.6 26.1

mxvNaiveTransposeConstant 矩阵转置 +constant memory

4.2 28.6

mxvNaiveTransposeShared 矩阵转置 +shared memory

2.6 46.2

mxvBlock block模式 5.4 22.2

mxvWarp warp模式 4.1 29.3

cublas 调用 cublasSgemv函数 2.6 46.226

Page 27: 矩阵与向量乘法的 CUDA 优化

总结一下(续)• 矩阵转置以满足合并访问• 使用常量存储器,共享存储器• 使用 block模式和 warp模式

27

其它的一些优化方法手动循环展开数据预取指令混合

Page 28: 矩阵与向量乘法的 CUDA 优化

感谢 itpub提供的这次机会 ,谢谢大家 ,欢迎提问 !

28