使用CUDA减少矩阵行

使用CUDA减少矩阵行,c,matrix,cuda,C,Matrix,Cuda,我编写了一个简单的CUDA代码,用于计算矩阵的行和。 矩阵具有指向浮点的一维表示指针 代码的串行版本如下所示,如预期的那样,它有2个循环: Windows 7, NVidia GeForce 425M. 到目前为止还不错。串行和并行CUDA结果相同 关键是CUDA版本的计算时间几乎是串行版本的两倍,即使我更改了nThreadsPerBlock参数:我测试了nThreadsPerBlock,将我的卡允许的每个块的最大线程数从32个增加到1024个 在我看来,矩阵维度足够大,足以证明并行化的合理性

我编写了一个简单的CUDA代码,用于计算矩阵的行和。 矩阵具有指向浮点的一维表示指针

代码的串行版本如下所示,如预期的那样,它有2个循环:

Windows 7, NVidia GeForce 425M.
到目前为止还不错。串行和并行CUDA结果相同

关键是CUDA版本的计算时间几乎是串行版本的两倍,即使我更改了nThreadsPerBlock参数:我测试了nThreadsPerBlock,将我的卡允许的每个块的最大线程数从32个增加到1024个

在我看来,矩阵维度足够大,足以证明并行化的合理性:90000 x 1000

下面,我使用不同的nThreadsPerBlock报告串行和并行版本所用的时间。在平均100个样本上报告的时间(毫秒):

矩阵:nrow=90000 x ncol=1000

串行:每个样本经过的平均时间(毫秒),100个样本:289.18

CUDA 32 ThreadsPerBlock:每个样本经过的平均时间(毫秒)100个样本:497.11

CUDA 1024 ThreadsPerBlock:每个样本经过的平均时间(毫秒)100个样本:699.66

以防万一,具有32/1024 nThreadsPerBlock的版本是最快/最慢的版本

我知道从主机复制到设备时会有一种开销,反之亦然,但速度慢可能是因为我没有实现最快的代码

因为我远非CUDA专家:

我是否为此任务编写了最快的版本?如何改进代码? 我可以去掉内核函数中的循环吗

任何想法都值得赞赏

编辑1 虽然我描述了一个标准的行和,但我对具有0的行的和/或操作感兴趣;1} 值,如行和/行或。也就是说,正如一些评论员所建议的那样,它不允许我利用cuBLAS乘以1的列向量技巧

编辑2 根据用户的建议,其他用户在此认可:


忘了尝试编写自己的函数,改用推力库,神奇就来了。

如果这是您需要对这些数据执行的操作行的总和,我不会期望GPU带来很大的好处。每个数据元素只有一个算术运算,为此您需要支付将该数据元素传输到GPU的费用。除了某个问题的大小,不管怎样让机器保持忙碌,你都不会从更大的问题大小中得到额外的好处,因为算术强度是开着的

因此,在GPU上解决这个问题并不是一个特别令人兴奋的问题

但正如Talonmes所指出的那样,您在构建过程中遇到了一个凝聚问题,这将进一步降低速度。让我们来看一个小例子:

__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;            
    }

}
上面是矩阵一小部分的简单图示示例。机器数据存储器使得元件11、12、13和14存储在相邻的存储器位置中

对于联合访问,我们需要一种访问模式,以便从同一条指令请求相邻的内存位置,并跨扭曲执行

我们需要从扭曲的角度考虑代码的执行,即在锁步中执行32个线程。你的代码在做什么?它在每个步骤/指令中要求哪些元素?让我们看一下这行代码:

    C1  C2  C3  C4
R1  11  12  13  14
R2  21  22  23  24
R3  31  32  33  34
R4  41  42  43  44
创建该变量时,扭曲中的相邻线程具有相邻值,即rowIdx的连续值。那么,当k=0时,当我们试图检索值m[rowIdx*ncol+k]时,每个线程请求哪个数据元素

在块0中,线程0的rowIdx为0。线程1的rowIdx为1,以此类推。因此,此指令中每个线程要求的值为:

        sum+=m[rowIdx*ncol+k];
但这不是联合访问!元素11、21等在存储器中不相邻。对于合并访问,我们希望矩阵元素行如下所示:

Thread:   Memory Location:    Matrix Element:
     0      m[0]                   (11)
     1      m[ncol]                (21)
     2      m[2*ncol]              (31)
     3      m[3*ncol]              (41)
Thread:   Memory Location:    Matrix Element:
     0      m[?]                   (11)
     1      m[?]                   (12)
     2      m[?]                   (13)
     3      m[?]                   (14)
如果你然后反向工作,确定什么价值的?应该是这样的,你会想出一个类似这样的指示:

Thread:   Memory Location:    Matrix Element:
     0      m[0]                   (11)
     1      m[ncol]                (21)
     2      m[2*ncol]              (31)
     3      m[3*ncol]              (41)
Thread:   Memory Location:    Matrix Element:
     0      m[?]                   (11)
     1      m[?]                   (12)
     2      m[?]                   (13)
     3      m[?]                   (14)
这将提供联合访问,但不会给出正确的答案,因为我们现在对矩阵列求和,而不是对矩阵行求和。我们可以通过将数据存储重新组织为列主顺序而不是行主顺序来解决这个问题。你应该可以在谷歌上搜索创意,对吗?从概念上讲,这相当于转置矩阵m。在我看来,这是否方便你去做,不在你的问题范围之内,也不是中大的问题。在主机上创建矩阵或将矩阵从主机传输到设备时,这可能是一件简单的事情。但总的来说,如果矩阵是按行大顺序存储的,我不知道有什么方法可以用100%合并访问对矩阵行求和。你可以采取一系列的排减法,但我觉得这很痛苦

这并不罕见,当我们在思考加速发展的方法时 GPU上的码率代码,考虑重新组织我们的数据存储,方便GPU。这是一个例子

而且,是的,我在这里概述的仍然在内核中保留一个循环


作为补充说明,我建议分别对数据拷贝部分和内核计算部分进行计时。我无法从你的问题中判断你是在给内核计时还是给整个GPU操作计时,包括数据拷贝。如果单独计算数据拷贝的时间,您可能会发现只有数据拷贝时间超过了CPU时间。任何优化CUDA代码的努力都不会影响数据复制时间。在您花费大量时间之前,这可能是一个有用的数据点。

因为您提到,您需要的是通用的简化算法,而不仅仅是求和算法。这里我将尝试给出3种方法。内核方法可能具有最高的性能。推力方法最容易实现。cuBLAS方法仅适用于sum,具有良好的性能

核方法 介绍如何优化标准并行归约。标准还原可分为两个阶段

多个线程块,每个线程块减少一部分数据; 一个螺纹块从第1阶段的结果减少到最后1个元素。 对于您的多次减少铺层行数问题,仅第1阶段就足够了。其思想是每个线程块减少1行。有关更多注意事项,如每线程块多行或每多线程块1行,可以参考。这可能会进一步提高性能,尤其是对于形状不好的矩阵

推力进近 一般的多重减速可以在几分钟内通过推力::减速键完成。你可以在这里找到一些讨论

但是,通过_键减少_并不假定每一行的长度相同,因此您将得到性能惩罚。另一篇文章在行和上给出了推力::按_键减少_和cuBLAS方法之间的概要比较。它可以让你对表演有一个基本的了解

库布拉斯法 矩阵a的行/列之和可视为矩阵向量乘法,其中向量的元素均为1。它可以由以下matlab代码表示

        sum+=m[k*ncol+rowIdx];
其中y是A的行之和

cuBLAS库为此操作提供了高性能矩阵向量乘法函数

计时结果表明,该例程仅比一次读取一个矩阵的所有元素慢10~50%,这可以看作是该操作性能的理论上限。

使用CUDA推力可以通过三种方式解决减少矩阵行的问题,它们可能不是唯一的方式,但解决这一点已经超出了范围。同一OP也认识到,对于此类问题,使用CUDA推力更为可取。此外,使用cuBLAS的方法也是可能的

方法1-按键减少_

这是本次会议提出的方法。它包括一个使用make_discard_迭代器的变量

方法2-变换

这是罗伯特·克罗维拉在华盛顿大学提出的方法

方法3-通过按键进行包容性扫描

这是Eric at建议的方法

方法4-cublasgemv

它使用cuBLAS gemv将相关矩阵乘以一列1

完整代码

下面是浓缩这两种方法的代码。Utilities.cu和Utilities.cuh文件在此处被标记和省略。TimingGPU.cu和TimingGPU.cuh将被保留,并被省略

y = A * ones(size(A,2),1);

似乎方法1和3的性能优于方法2,但列数较少的情况除外。但是,最好的方法是方法4,它比其他方法方便得多,前提是创建计划所需的时间可以在计算过程中摊销。

Google CUDA memory coalescing and start reading-这就是你的问题所在。@Talonmes,非常感谢。如果它没有bug你,你能用一段代码来回答这个问题吗?它仍然有一个内部内核循环吗?通过减少操作可以有效地并行执行数组元素的求和。在您的例子中,可以按行应用缩减,这样所涉及的数组将是不同的行。也许,除了处理Talonmes建议的合并之外,还可以看看SDK的缩减示例?@jackolanten,谢谢您的评论。如果可能的话,请您使用reduce粘贴/回答一段代码?我猜你指的是使用推力库的操作,我错了吗?根据矩阵向量乘法,索伦森写了一篇很好的论文,根据矩阵的形状,从该操作中获得最大性能:@Eric我在前面的StackOverflow问题中看到了这个想法。我提到行和是因为它是标准的,但我真正能做的是行和/或运算,而不是和。我不能用你的伎俩。无论如何
s、 非常感谢您尝试回答我的问题。@Novak非常好的论文。它使用不同的内核来处理不同的情况。Cublas例行公事可能只考虑一般好的情况……既不太高也不太好。wide@ChuckKillerDoll一般的多重归约可以通过推力::归约键来完成。您可以在这里找到一些关于最高性能的讨论,您仍然需要编写自己的内核。标准约简核是一个很好的多重约简模板,因为多重约简实际上是标准约简的第一步。稍后我可能会补充一些内容。首先,感谢您澄清联合内存访问的含义。当你说:…我不知道如何用100%合并访问来求矩阵行的和,如果矩阵是按行主顺序存储的。。。我有这样一个想法:sum+=m[blockIdx.x*ncol+threadIdx.x],在这里用threadIdx.x==ncol启动内核。在这种情况下,内存访问不是合并的吗?threadIdx.x==ncol没有意义。你不能选择threadIdx.x。我假设您的意思是每个块的线程数==ncol。您重新定义的sum确实在生成联合访问,但您仍然在对列进行求和,至少参考原始存储格式假定的行主顺序。既然你的求和不再依赖于循环变量k,那么在你的问题的上下文中提出这种改变真的有点傻。在评论中提出新问题很难处理。我真的认为@Eric给了你一个很好的答案。我把它投了更高的票。@Eric显然我的意思是用block==ncol调用内核。我不是在问一个新问题,我只是在问你对使用sum+=m[blockIdx.x*ncol+threadIdx.x]的想法有什么看法,如果你这样安排,你就是在合并,因为你在扫描相邻的内存元素,给定向量是按行部署的。我不是Eric。我不清楚你的意思是block==ncol。我已经说过,您将获得联合访问,但不是您想要的答案,因为您基本上是对列而不是行求和。正如我已经说过的,这一行代码需要一个经过修改的内核才能发挥作用。将这一行放到您提出的现有内核代码中是没有意义的。实际上,每个块都在一行上工作。我不清楚在没有还原技术(即不同的内核)的情况下,如何实现求和或任何您选择的操作。很抱歉Eric提到@RobertCrovella。为什么我说它需要内存的相邻地址,是因为如果你使用sum+=m[blockIdx.x*ncol+threadIdx.x],看看块0,线程0,你有m[0],如果你选择块0,线程1,你有m[1],依此类推。当您移动到下一个块时,您有:块1,线程1 m[ncol],块1,线程1 m[ncol+1]。为什么不合并呢?记忆力
#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;
}
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