Windows 7, NVidia GeForce 425M.
我写了一个简单的CUDA代码,用于计算矩阵的行和。 矩阵具有单维表示(指向浮点的指针)。
代码的串行版本如下(它有2
个循环,如预期的那样):
void serial_rowSum (float* m, float* output, int nrow, int ncol) {
float sum;
for (int i = 0 ; i < nrow ; i++) {
sum = 0;
for (int j = 0 ; j < ncol ; j++)
sum += m[i*ncol+j];
output[i] = sum;
}
}
在CUDA代码中,我调用内核函数按行扫描矩阵。下面是内核调用片段:
dim3 threadsPerBlock((unsigned int) nThreadsPerBlock); // has to be multiple of 32
dim3 blocksPerGrid((unsigned int) ceil(nrow/(float) nThreadsPerBlock));
kernel_rowSum<<<blocksPerGrid, threadsPerBlock>>>(d_m, d_output, nrow, ncol);
和执行行的并行求和的内核函数(仍然有1
循环):
__global__ void kernel_rowSum(float *m, float *s, int nrow, int ncol) {
int rowIdx = threadIdx.x + blockIdx.x * blockDim.x;
if (rowIdx < nrow) {
float sum=0;
for (int k = 0 ; k < ncol ; k++)
sum+=m[rowIdx*ncol+k];
s[rowIdx] = sum;
}
}
到目前为止一切顺利。串行和并行(CUDA)结果相同。
重点是,即使我更改了nThreadsPerBlock
参数,CUDA版本也需要几倍于序列号计算的时间:我从nThreadsPerBlock
测试32
到{ {1}}(我的卡允许的每个块的最大线程数)。
IMO,矩阵维度足以证明并行化的合理性:1024
。
下面,我将报告使用不同90,000 x 1,000
的串行和并行版本所用的时间。 nThreadsPerBlock
报告的时间平均为msec
个样本:
矩阵:nrow = 90000 x ncol = 1000
序列号:每个样本经过的平均时间,以毫秒为单位(100
个样本):100
。
CUDA(289.18
ThreadsPerBlock):每个样本经过的平均时间,以毫秒为单位(32
个样本):100
。
CUDA(497.11
ThreadsPerBlock):每个样本经过的平均时间,以毫秒为单位(1024
个样本):100
。
以防万一,699.66
/ 32
1024
的版本是最快/最慢的版本。
我知道从主机复制到设备时有一种开销,反之亦然,但可能是因为我没有实现最快的代码。
因为我不是一名CUDA专家:
我是否为此任务编写了最快的版本?我怎么能改进我的代码? 我可以摆脱内核函数中的循环吗?
任何想法都赞赏。
虽然我描述了一个标准nThreadsPerBlock
,但我对rowSum
/ AND
行的OR
/ (0;1}
操作感兴趣,这些行的rowAND
值为rowOR
{1}}。也就是说,根据一些评论员的建议,它不允许我利用cuBLAS
乘以1
的{{1}}列向量技巧。
正如用户建议其他用户并在此认可:
忘记尝试编写自己的功能,使用Thrust库代替魔法来。
答案 0 :(得分:13)
因为你提到你只需要总和还原算法。我会尝试在这里提供3种方法。内核方法可能具有最高性能。推力方法最容易实现。 cuBLAS方法仅适用于sum并且具有良好的性能。
Here's a very good doc介绍如何优化标准并行缩减。标准减少可分为两个阶段。
对于多重减少(减少垫子的行)问题,只有第1阶段就足够了。这个想法是每个线程块减少1行。有关每个线程块多行或每个多线程块1行的进一步注意事项,可以参考paper provided by @Novak。这可以更好地改善性能,特别是对于形状不好的矩阵。
一般多次缩减可以在几分钟内由thrust::reduction_by_key
完成。你可以在Determining the least element and its position in each matrix column with CUDA Thrust找到一些讨论。
但是thrust::reduction_by_key
并不假设每行具有相同的长度,因此您将获得性能损失。另一篇文章How to normalize matrix columns in CUDA with max performance?给出了thrust::reduction_by_key
和cuBLAS方法之间的行总和的性能比较。它可以让您对性能有基本的了解。
矩阵A的行/列的总和可以看作矩阵向量乘法,其中向量的元素都是1。它可以用以下matlab代码表示。
y = A * ones(size(A,2),1);
其中y
是A的行总和。
cuBLAS libary为此操作提供了高性能的矩阵向量乘法函数cublas<t>gemv()
。
时序结果表明,该程序只比简单读取A的所有元素慢10~50%,这可以看作是该操作性能的理论上限。
答案 1 :(得分:4)
减少矩阵的行可以通过三种方式使用 CUDA Thrust 来解决(它们可能不是唯一的,但解决这一问题超出了范围) )。同样的OP也认识到,使用CUDA Thrust对于这种问题是优选的。此外,可以使用 cuBLAS 的方法。
方法#1 - reduce_by_key
这是Thrust example page建议的方法。它包含使用make_discard_iterator
的变体。
方法#2 - transform
这是Robert Crovella在CUDA Thrust: reduce_by_key on only some values in an array, based off values in a “key” array建议的方法。
方法#3 - inclusive_scan_by_key
这是Eric在How to normalize matrix columns in CUDA with max performance?建议的方法。
方法#4 - cublas<t>gemv
它使用cuBLAS
gemv
将相关矩阵乘以1
列。
完整代码
这是缩小两种方法的代码。 Utilities.cu
和Utilities.cuh
文件被隐藏here,此处省略。 TimingGPU.cu
和TimingGPU.cuh
维护here,也会被省略。
#include <cublas_v2.h>
#include <thrust/host_vector.h>
#include <thrust/device_vector.h>
#include <thrust/generate.h>
#include <thrust/reduce.h>
#include <thrust/functional.h>
#include <thrust/random.h>
#include <thrust/sequence.h>
#include <stdio.h>
#include <iostream>
#include "Utilities.cuh"
#include "TimingGPU.cuh"
// --- Required for approach #2
__device__ float *vals;
/**************************************************************/
/* CONVERT LINEAR INDEX TO ROW INDEX - NEEDED FOR APPROACH #1 */
/**************************************************************/
template <typename T>
struct linear_index_to_row_index : public thrust::unary_function<T,T> {
T Ncols; // --- Number of columns
__host__ __device__ linear_index_to_row_index(T Ncols) : Ncols(Ncols) {}
__host__ __device__ T operator()(T i) { return i / Ncols; }
};
/******************************************/
/* ROW_REDUCTION - NEEDED FOR APPROACH #2 */
/******************************************/
struct row_reduction {
const int Ncols; // --- Number of columns
row_reduction(int _Ncols) : Ncols(_Ncols) {}
__device__ float operator()(float& x, int& y ) {
float temp = 0.f;
for (int i = 0; i<Ncols; i++)
temp += vals[i + (y*Ncols)];
return temp;
}
};
/**************************/
/* NEEDED FOR APPROACH #3 */
/**************************/
template<typename T>
struct MulC: public thrust::unary_function<T, T>
{
T C;
__host__ __device__ MulC(T c) : C(c) { }
__host__ __device__ T operator()(T x) { return x * C; }
};
/********/
/* MAIN */
/********/
int main()
{
const int Nrows = 5; // --- Number of rows
const int Ncols = 8; // --- Number of columns
// --- Random uniform integer distribution between 10 and 99
thrust::default_random_engine rng;
thrust::uniform_int_distribution<int> dist(10, 99);
// --- Matrix allocation and initialization
thrust::device_vector<float> d_matrix(Nrows * Ncols);
for (size_t i = 0; i < d_matrix.size(); i++) d_matrix[i] = (float)dist(rng);
TimingGPU timerGPU;
/***************/
/* APPROACH #1 */
/***************/
timerGPU.StartCounter();
// --- Allocate space for row sums and indices
thrust::device_vector<float> d_row_sums(Nrows);
thrust::device_vector<int> d_row_indices(Nrows);
// --- Compute row sums by summing values with equal row indices
//thrust::reduce_by_key(thrust::make_transform_iterator(thrust::counting_iterator<int>(0), linear_index_to_row_index<int>(Ncols)),
// thrust::make_transform_iterator(thrust::counting_iterator<int>(0), linear_index_to_row_index<int>(Ncols)) + (Nrows*Ncols),
// d_matrix.begin(),
// d_row_indices.begin(),
// d_row_sums.begin(),
// thrust::equal_to<int>(),
// thrust::plus<float>());
thrust::reduce_by_key(
thrust::make_transform_iterator(thrust::make_counting_iterator(0), linear_index_to_row_index<int>(Ncols)),
thrust::make_transform_iterator(thrust::make_counting_iterator(0), linear_index_to_row_index<int>(Ncols)) + (Nrows*Ncols),
d_matrix.begin(),
thrust::make_discard_iterator(),
d_row_sums.begin());
printf("Timing for approach #1 = %f\n", timerGPU.GetCounter());
// --- Print result
for(int i = 0; i < Nrows; i++) {
std::cout << "[ ";
for(int j = 0; j < Ncols; j++)
std::cout << d_matrix[i * Ncols + j] << " ";
std::cout << "] = " << d_row_sums[i] << "\n";
}
/***************/
/* APPROACH #2 */
/***************/
timerGPU.StartCounter();
thrust::device_vector<float> d_row_sums_2(Nrows, 0);
float *s_vals = thrust::raw_pointer_cast(&d_matrix[0]);
gpuErrchk(cudaMemcpyToSymbol(vals, &s_vals, sizeof(float *)));
thrust::transform(d_row_sums_2.begin(), d_row_sums_2.end(), thrust::counting_iterator<int>(0), d_row_sums_2.begin(), row_reduction(Ncols));
printf("Timing for approach #2 = %f\n", timerGPU.GetCounter());
for(int i = 0; i < Nrows; i++) {
std::cout << "[ ";
for(int j = 0; j < Ncols; j++)
std::cout << d_matrix[i * Ncols + j] << " ";
std::cout << "] = " << d_row_sums_2[i] << "\n";
}
/***************/
/* APPROACH #3 */
/***************/
timerGPU.StartCounter();
thrust::device_vector<float> d_row_sums_3(Nrows, 0);
thrust::device_vector<float> d_temp(Nrows * Ncols);
thrust::inclusive_scan_by_key(
thrust::make_transform_iterator(thrust::make_counting_iterator(0), linear_index_to_row_index<int>(Ncols)),
thrust::make_transform_iterator(thrust::make_counting_iterator(0), linear_index_to_row_index<int>(Ncols)) + (Nrows*Ncols),
d_matrix.begin(),
d_temp.begin());
thrust::copy(
thrust::make_permutation_iterator(
d_temp.begin() + Ncols - 1,
thrust::make_transform_iterator(thrust::make_counting_iterator(0), MulC<int>(Ncols))),
thrust::make_permutation_iterator(
d_temp.begin() + Ncols - 1,
thrust::make_transform_iterator(thrust::make_counting_iterator(0), MulC<int>(Ncols))) + Nrows,
d_row_sums_3.begin());
printf("Timing for approach #3 = %f\n", timerGPU.GetCounter());
for(int i = 0; i < Nrows; i++) {
std::cout << "[ ";
for(int j = 0; j < Ncols; j++)
std::cout << d_matrix[i * Ncols + j] << " ";
std::cout << "] = " << d_row_sums_3[i] << "\n";
}
/***************/
/* APPROACH #4 */
/***************/
cublasHandle_t handle;
timerGPU.StartCounter();
cublasSafeCall(cublasCreate(&handle));
thrust::device_vector<float> d_row_sums_4(Nrows);
thrust::device_vector<float> d_ones(Ncols, 1.f);
float alpha = 1.f;
float beta = 0.f;
cublasSafeCall(cublasSgemv(handle, CUBLAS_OP_T, Ncols, Nrows, &alpha, thrust::raw_pointer_cast(d_matrix.data()), Ncols,
thrust::raw_pointer_cast(d_ones.data()), 1, &beta, thrust::raw_pointer_cast(d_row_sums_4.data()), 1));
printf("Timing for approach #4 = %f\n", timerGPU.GetCounter());
for(int i = 0; i < Nrows; i++) {
std::cout << "[ ";
for(int j = 0; j < Ncols; j++)
std::cout << d_matrix[i * Ncols + j] << " ";
std::cout << "] = " << d_row_sums_4[i] << "\n";
}
return 0;
}
时间结果(在Kepler K20c上测试)
Matrix size #1 #1-v2 #2 #3 #4 #4 (no plan)
100 x 100 0.63 1.00 0.10 0.18 139.4 0.098
1000 x 1000 1.25 1.12 3.25 1.04 101.3 0.12
5000 x 5000 8.38 15.3 16.05 13.8 111.3 1.14
100 x 5000 1.25 1.52 2.92 1.75 101.2 0.40
5000 x 100 1.35 1.99 0.37 1.74 139.2 0.14
似乎方法#1和#3优于方法#2,除了少量列的情况。然而,最好的方法是方法#4,它比其他方法更方便,前提是创建计划所需的时间可以在计算过程中摊销。
答案 2 :(得分:3)
如果这是您需要对此数据执行的操作的范围(对行进行求和),我不希望GPU获得相当大的好处。每个数据元素只有一个算术运算,为此您需要支付将该数据元素传输到GPU的成本。除了一定的问题大小(无论如何保持机器繁忙),您都无法从更大的问题大小中获得额外的好处,因为算术强度为O(n)。
因此,这不是GPU上需要解决的特别激动人心的问题。
但正如talonmies所指出的那样,你制作它的方式存在一个合并的问题,这会进一步降低速度。我们来看一个小例子:
C1 C2 C3 C4
R1 11 12 13 14
R2 21 22 23 24
R3 31 32 33 34
R4 41 42 43 44
以上是矩阵中一小部分的简单图示示例。机器数据存储使得元件(11),(12),(13)和(14)存储在相邻的存储器位置中。
对于合并访问,我们需要一种访问模式,以便从相同的指令请求相邻的内存位置,并在整个warp中执行。
我们需要考虑从 warp 的角度执行代码,即在锁定步骤中执行的32个线程。你的代码在做什么?它在每个步骤/指令中检索(要求)哪些元素?我们来看看这行代码:
sum+=m[rowIdx*ncol+k];
当您创建该变量时,warp中的相邻线程具有rowIdx
的相邻(即连续)值。因此,当k
= 0时,当我们尝试检索值m[rowIdx*ncol+k]
时,每个线程都会询问哪个数据元素?
在块0中,线程0的rowIdx
为0.线程1的rowIdx
为1,等等。因此,每条线程在此指令中要求的值为:
Thread: Memory Location: Matrix Element:
0 m[0] (11)
1 m[ncol] (21)
2 m[2*ncol] (31)
3 m[3*ncol] (41)
但这不是合并访问!元件(11),(21)等在存储器中不相邻。对于合并访问,我们希望Matrix Element行如下所示:
Thread: Memory Location: Matrix Element:
0 m[?] (11)
1 m[?] (12)
2 m[?] (13)
3 m[?] (14)
如果然后向后工作以确定?
的值应该是什么,那么你将会得到类似这样的指令:
sum+=m[k*ncol+rowIdx];
这将提供合并访问权限,但它不会给出正确答案,因为我们现在将矩阵列而不是矩阵行相加。我们可以通过将数据存储重新组织为列主要顺序而不是行主顺序来解决此问题。 (你应该可以谷歌那个想法,对吗?)从概念上讲,这相当于转置你的矩阵m
。这对您来说是否方便,超出了您的问题的范围,正如我所看到的那样,而不是真正的CUDA问题。当您在主机上创建矩阵或将矩阵从主机传输到设备时,您可能会做一件简单的事情。但总的来说,如果矩阵以行主顺序存储,我不知道如何将矩阵行与100%合并访问相加。 (您可以采用一系列行减少但这看起来很痛苦。)
当我们考虑在GPU上加速代码的方法时,考虑重新组织我们的数据存储以促进GPU,这并不罕见。这是一个例子。
而且,是的,我在这里概述的内容仍然在内核中保留一个循环。
作为补充评论,我建议分别对数据复制部分和内核(计算)部分进行计时。我无法从您的问题中判断出您是仅计时内核还是整个(GPU)操作,包括数据副本。如果单独计算数据副本的时间,您可能会发现只有数据复制时间超过了CPU时间。优化CUDA代码的任何努力都不会影响数据复制时间。在您花费大量时间之前,这可能是一个有用的数据点。