新手请教CUDA优化问题

我自己设计了一个计算一组数据的多项式的值的小程序,用来学习CUDA的优化,线程代码如下。

#define THREAD_NUM_PER_BLOCK 512 //每个块的线程数
#define MAX_BLOCK 65535 //NVS 5400m允许的最大块数
//用cuda计算单个采样点的多项式值
nGrid: 输入,本采样点所在的第几个Grid
pfSamples: 输入,存放采样点的值
pfVolumes: 输出,存放采样点多项式的值
nSampleNum: 输入,采样点个数
pfPolyCoefs:输入,多项式的系数
nCoefNum: 输入,多项式的维数//
/
global void cudaGetPolyVol( int nGrid, float * pfSamples, float * pfVolumes, int nSampleNum, float * pfPolyCoefs, int nCoefNum )
{
int nTid = nGrid * MAX_BLOCK * THREAD_NUM_PER_BLOCK + blockIdx.xTHREAD_NUM_PER_BLOCK + threadIdx.x;
if( nTid < nSampleNum )
{
float x = pfSamples[ nTid ];
float y = pfPolyCoefs[ nCoefNum - 1 ];
for( int i=nCoefNum-2; i>=0; i-- )
{
y = y
x + pfPolyCoefs[ i ];
}
pfVolumes[ nTid ] = y;
}
__syncthreads();
}

现有几个问题请教一下:
(1)多项式系数pfPolyCoefs是从计算机内存拷贝到显存中的,是不是存在全局内存中?用y = y*x + pfPolyCoefs[ i ];是不是速度很慢?怎样把它变成共享内存?
(2)float x = pfSamples[ nTid ];这一句是不是就把全局内存中的数复制到了寄存器中?pfVolumes[ nTid ] = y;这一句是不是因为一个线程只调用一次就不必要再优化了?
(3)有没有现成的计算数组的和、解线性方程组以及BP神经网络相关的函数?
谢谢各位大侠了。

LZ您好:

您的各个问题分别回答如下:

1:“多项式系数pfPolyCoefs是从计算机内存拷贝到显存中的,是不是存在全局内存中?”,如果您是使用cudaMemcpy()复制的,那么会复制到device端的global memory中。
“用y = y*x + pfPolyCoefs[ i ];是不是速度很慢?怎样把它变成共享内存?”,这一句仅就访存而言,不算太差,但是也浪费了一些带宽。如果需要将pfpolyCoefs复制到shared memory,可以如下:

在您的kernel开始的地方写
shared int pfpolyCoefs_s[nCeofNum];
int tid=threadIdx.x; //假定您是按照一维block使用的,如果不是请适当修改
if(tid<nCoefNum)
{
pfpolyCoefs_s[tid]=pfpolyCoefs[tid];
//如果您的多项式的阶数比一个block内的threads数量还多请做适当修改
}
__syncthreads();

在这个后面就可以使用pfpolyCoefs_s来代替原来的pfpolyCoefs访问了。

以及,您的pfpolyCoefs也可以考虑使用constant memory存储。

以及您的多项式如果阶数不多而且固定的话,也可以考虑直接写好展开的式子。

贴出全部代码可能能更好地表达我的意思,供各位大侠参考。

#include “cuda_runtime.h”
#include “device_launch_parameters.h”

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <time.h>
#include <Windows.h>

#define THREAD_NUM_PER_BLOCK 512 //每个块的线程数
#define MAX_BLOCK 65535 //NVS 5400m允许的最大块数

//用cuda计算单个采样点的多项式值
nGrid: 输入,本采样点所在的第几个Grid
pfSamples: 输入,存放采样点的值
pfVolumes: 输出,存放采样点多项式的值
nSampleNum: 输入,采样点个数
pfPolyCoefs:输入,多项式的系数
nCoefNum: 输入,多项式的维数//
/
global void cudaGetPolyVol( int nGrid, float * pfSamples, float * pfVolumes, int nSampleNum, float * pfPolyCoefs, int nCoefNum )
{
int nTid = nGrid * MAX_BLOCK * THREAD_NUM_PER_BLOCK + blockIdx.xTHREAD_NUM_PER_BLOCK + threadIdx.x;
if( nTid < nSampleNum )
{
float x = pfSamples[ nTid ];
float y = pfPolyCoefs[ nCoefNum - 1 ];
for( int i=nCoefNum-2; i>=0; i-- )
{
y = y
x + pfPolyCoefs[ i ];
}
pfVolumes[ nTid ] = y;
}
__syncthreads();
}

int main()
{
//创建采样点、多项式系数数组,并用随机数赋值
int nSampleNum = 2000000*6, nCoefNum = 1000;
float * pfSamples = (float *)malloc( sizeof(float)*nSampleNum );
float * pfGpuVolumes = (float *)malloc( sizeof(float)*nSampleNum );
float * pfCpuVolumes = (float *)malloc( sizeof(float)*nSampleNum );
float * pfCoefs = (float *)malloc( sizeof(float)*nCoefNum );
if( 0 == pfSamples || 0 == pfGpuVolumes || 0 == pfCpuVolumes || 0 == pfCoefs ) goto Error;
srand( (unsigned)time( NULL ) );
for( int i=0; i<nSampleNum; i++ )
{
pfSamples[ i ] = (rand()/32767.0f) * (rand()/32767.0f);
pfGpuVolumes[ i ] = 0.0f;
pfCpuVolumes[ i ] = 0.0f;
}
for( int i=0; i<nCoefNum; i++ ) pfCoefs[ i ] = rand()/10000.0f;

//以下用CUDA计算多项式的值,并估算耗费时间
int nBlockNum = nSampleNum/THREAD_NUM_PER_BLOCK + 1;
int nGridNum = nBlockNum/MAX_BLOCK + 1;
float * pfDeviceSamples = 0;
float * pfDeviceVolumes = 0;
float * pfDeviceCoefs = 0;

cudaError_t cudaStatus;

// Choose which GPU to run on, change this on a multi-GPU system.
cudaStatus = cudaSetDevice( 0 );
if (cudaStatus != cudaSuccess) {
fprintf(stderr, “cudaSetDevice failed! Do you have a CUDA-capable GPU installed?”);
goto Error;
}

// Allocate GPU buffers for three vectors (two input, one output) .
cudaStatus = cudaMalloc( (void**)&pfDeviceSamples, nSampleNum * sizeof(float) );
if (cudaStatus != cudaSuccess) {
fprintf(stderr, “cudaMalloc failed!”);
goto Error;
}

cudaStatus = cudaMalloc( (void**)&pfDeviceVolumes, nSampleNum * sizeof(float) );
if (cudaStatus != cudaSuccess) {
fprintf(stderr, “cudaMalloc failed!”);
goto Error;
}

cudaStatus = cudaMalloc( (void**)&pfDeviceCoefs, nCoefNum * sizeof(float) );
if (cudaStatus != cudaSuccess) {
fprintf(stderr, “cudaMalloc failed!”);
goto Error;
}

// Copy input vectors from host memory to GPU buffers.
cudaStatus = cudaMemcpy( pfDeviceSamples, pfSamples, nSampleNum * sizeof(float), cudaMemcpyHostToDevice );
if (cudaStatus != cudaSuccess) {
fprintf(stderr, “cudaMemcpy failed!”);
goto Error;
}

cudaStatus = cudaMemcpy( pfDeviceVolumes, pfGpuVolumes, nSampleNum * sizeof(float), cudaMemcpyHostToDevice );
if (cudaStatus != cudaSuccess) {
fprintf(stderr, “cudaMemcpy failed!”);
goto Error;
}

cudaStatus = cudaMemcpy( pfDeviceCoefs, pfCoefs, nCoefNum * sizeof(float), cudaMemcpyHostToDevice );
if (cudaStatus != cudaSuccess) {
fprintf(stderr, “cudaMemcpy failed!”);
goto Error;
}

// Launch a kernel on the GPU with one thread for each element.
printf( “Getting using GPU…” );
DWORD dwGpuBeginning = GetTickCount();
for( int i=0; i<nGridNum; i++ )
{
cudaGetPolyVol<<<MAX_BLOCK, THREAD_NUM_PER_BLOCK>>>( i, pfDeviceSamples, pfDeviceVolumes, nSampleNum,
pfDeviceCoefs, nCoefNum );

	// Check for any errors launching the kernel
	cudaStatus = cudaGetLastError();
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "addKernel launch failed: %s\n", cudaGetErrorString(cudaStatus));
		goto Error;
	}

	// cudaDeviceSynchronize waits for the kernel to finish, and returns
	// any errors encountered during the launch.
	cudaStatus = cudaDeviceSynchronize();
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaDeviceSynchronize returned error code %d after launching addKernel!\n", cudaStatus);
		goto Error;
	}
}

// Copy output vector from GPU buffer to host memory.
cudaStatus = cudaMemcpy( pfGpuVolumes, pfDeviceVolumes, nSampleNum * sizeof(float), cudaMemcpyDeviceToHost);
if (cudaStatus != cudaSuccess) {
	fprintf(stderr, "cudaMemcpy failed!");
	goto Error;

}
printf( “Ok. Time taken %d ms\n”, GetTickCount()-dwGpuBeginning );

// cudaDeviceReset must be called before exiting in order for profiling and
// tracing tools such as Nsight and Visual Profiler to show complete traces.
cudaStatus = cudaDeviceReset();
if (cudaStatus != cudaSuccess) {
fprintf(stderr, “cudaDeviceReset failed!”);
return 1;
}

//以下用CPU计算多项式的值,并估算耗时
printf( "Getting using CPU..." );
DWORD dwCpuBeginning = GetTickCount();
for( int i=0; i<nSampleNum; i++ )
{
	float x = pfSamples[ i ];
	float y = pfCoefs[ nCoefNum - 1 ];
	for( int k=nCoefNum-2; k>=0; k-- )
	{
		y = y*x + pfCoefs[ k ];
	}
	pfCpuVolumes[ i ] = y;
}
printf( "Ok. Time taken %d ms\n", GetTickCount()-dwCpuBeginning );

//将最后若干个计算结果输出,检查GPU的结果是否和CPU的一致
for( int i=0; i<200; i++ )
{
	int nSeries = nSampleNum-1-i;
	printf( "%15.10f,%15.10f,%15.10f\n", pfSamples[ nSeries ], pfGpuVolumes[ nSeries ], pfCpuVolumes[ nSeries ] );
}

Error:
cudaFree( pfDeviceSamples );
cudaFree( pfDeviceVolumes );
cudaFree( pfDeviceCoefs );
if( pfSamples ) free( pfSamples );
if( pfGpuVolumes ) free( pfGpuVolumes );
if( pfCpuVolumes ) free( pfCpuVolumes );
if( pfCoefs ) free( pfCoefs );

return 0;
}

2:“float x = pfSamples[ nTid ];这一句是不是就把全局内存中的数复制到了寄存器中?”,是的,这将实现一次global memory 的访存,并一般将访问得到的数据保存在寄存器中。
“pfVolumes[ nTid ] = y;这一句是不是因为一个线程只调用一次就不必要再优化了?”,这一句是将kernel内的临时变量y的值回写到global memory中。

上述两个指令都是一个线程只执行一次的,并且一个是初始读入数据,一个是最后回写数据,而且访问是合并的,所以这里可以不考虑优化。

3:有线性代数库——cublas,其他的库并不清楚,请LZ查阅本专业相关进展,寻找一下。

另外补充一下,您的代码一个线程只计算了一个点/多项式,您可以让每个线程干更多活。
以及,您一开始说的“#define MAX_BLOCK 65535 //NVS 5400m允许的最大块数”,这个应该是不对的,这个只是一个方向上(如x方向上)最大允许的block数量。而任何支持CUDA的硬件,一个grid都至少可以支持655356553565535个blocks(x,y,z方向上分别是65535)。kepler在x方向上更支持2^31-1个blocks。

您的问题大致回答如上,祝您编码顺利~

谢谢ice大及时、准确地解惑、纠错,受益匪浅。我按照您说的方法再改一下,看看优化效果如何。

以及,根据LZ 3#补充的代码,多项式的阶数是超过一个block中线程数的,那么给shared mempory赋值那里需要做一定修改。

例如改为

while(tid<nCoefNum)
{
pfpolyCoefs_s[tid]=pfpolyCoefs[tid];
tid+=blockDim.x;
//此时tid不再表示线程在block内的index,而只是辅助shared memory初始化寻址用
}

以及,您kernel结束前的那个__syncthreads()是不必要的。

哈哈!提供的信息越多,得到的信息也越多。谢谢ice大!

不客气的,经过我和横扫斑竹的讨论,觉得您这个实现框架大致上是可以的,您可以继续研究一下。您的代码写的很工整而清晰,是为发帖表率。

祝您编码顺利~

以及,玫瑰斑竹刚才私下提供了您的算法的一个改进方案,由我代为发表:

读取初始的y0,y1…y7
for( int i=nCoefNum-2; i>=0; i-- )
{
float co = coefs[…];
y0 = y0 * x + co;
y1 = y1 * x + co;
y2 = y2 * x + co;
y3 = y4 * x + co;
y4 = y3 * x + co;
y5 = y5 * x + co;
y6 = y6 * x + co;
y7 = y7 * x + co;
}

这样,可以在计算8组结果的时候,共用一些准备的数据,提高了计算/访存比例,更适合于GPU这种计算能力相对于访存能力强很多的架构。

在data layout上,您可以保持以前的安排不变,只是将以前8个grid的活一次都干了。
以及,您也可以适当挑选并行的数据,不一定是8组,4组也可以。
以及,您还需要根据您的数据总量进行一些修改,如果按照之前的安排的grid数量不是4的倍数,那么最后需要特殊处理一下,避免出问题。

大致如此,您可以适当参考。

欢迎您莅临cudazone@China,祝您编码顺利~

[
非常神奇!我将每个线程处理6个元素,耗费时间从810毫秒直降到250毫秒,但是将多项式系数拷入共享内存的优化效果不明显,但是拷贝方法对我很有启发----CUDA编程和CPU编程在思想上有较大差异,令人耳目一新。
谢谢诸位版主,我以后有什么问题不明白还要请教,望各位大佬不要嫌麻烦。

LZ您好:

1:每个线程处理多个元素带来的效能提升是可以预期的,因为这里将N次数据的准备工作减少到了1次,以及减少了多次重复启动kernel的时间。

2:因为原先直接从global memory读取数据的时候,还会被L2 cache缓冲,并不是每次都从global memory的DRAM中读取,以及当时的读取方式也没有warp内分支,而只是因为cache的读取粒度会造成一些浪费(如果您是kepler GPU,这个浪费会少一些,而且L2 cache得到了加强)。所以,综合看待计算和访存的比例,如果瓶颈并不在访存上,那么优化访存带来的好处就不多。以及,在一个线程处理多个元素的安排下,进一步降低了对访存的要求(因为读入一次可以使用6次)。

如果您在某个访存密集的问题中,将可以被局部反复利用的数据缓冲在shared memory中,那么会有比较明显的效果。

大致如此,欢迎您常来论坛,祝您好运~