草庐IT

c++ - 为三角矩阵计算优化 CUDA 内核的执行

coder 2024-02-11 原文

我正在开发我的第一个 Cuda 应用程序,我有一个“吞吐量低于预期”的内核,这似乎是目前最大的瓶颈。

内核的任务是计算一个 N × N 大小的矩阵 (DD),其中包含数据矩阵上所有元素之间的平方距离。数据矩阵 (Y) 的大小为 N x D(以支持多维数据)并存储为行优先。

来源:

__global__ void computeSquaredEuclideanDistance(const float * __restrict__ Y, float * __restrict__ DD, const int N, const int D) {
    int index = blockIdx.x * blockDim.x + threadIdx.x;
    int stride = blockDim.x * gridDim.x;

    for (int i = index; i < N * N; i += stride) {
        const int m = i / N;
        const int n = i % N;
        float tmp = 0;
        for (int d = 0; d < D; ++d) {
            const float Ynd = Y[d + D * n];
            const float Ymd = Y[d + D * m];
            const float Ydiff = Ynd - Ymd;
            tmp += Ydiff * Ydiff;
        }
        DD[n + N * m] = tmp;
    }
}

这是用 size_t blockSize = 256size_t numBlocks = (N*N + blockSize - 1)/blockSize 调用的。

如何优化这个内核?我最初的想法是,最耗时的部分是在不利用某种共享内存的情况下读取数据,但是谁能指导我如何处理这个问题?

nvvc 分析工具的注释:

  • 延迟分析:
    • 计算利用率约为 40%
    • 内存(二级缓存)利用率约为 35%
  • 入住不是问题
    • 理论 64 的 57.59 的有效扭曲
    • 入住率为理论 100 的 90%

对于我的应用程序,典型值是:

  • 5k N <>
  • D 是 2 或 3

最佳答案

我通常会忽略这些类型的优化问题,因为在我看来,它们处于离题边缘。最糟糕的是,你没有提供 MCVE所以任何试图回答的人都必须编写他们自己的所有支持代码来编译和基准测试你的内核。而这类工作确实需要基准测试和代码分析。但是因为你的问题基本上是一个线性代数问题(我喜欢线性代数),所以我回答了它而不是因为太宽泛而关闭投票......

说到这里。代码中会立即跳出一些可以改进的东西,并且可能会对运行时间产生重大影响。

首先是内循环的行程计数是先验已知的。任何时候遇到这种情况,请让编译器知道。循环展开和代码重新排序是一种非常强大的编译器优化,NVIDIA 编译器非常擅长。如果将 D 移动到模板参数中,则可以执行如下操作:

template<int D>
__device__ float esum(const float *x, const float *y)
{
    float val = 0.f;
#pragma unroll
    for(int i=0; i<D; i++) { 
        float diff = x[i] - y[i];
        val += diff * diff;
    }
    return val;
}

template<int D>
__global__ 
void vdistance0(const float * __restrict__ Y, float * __restrict__ DD, const int N)
{
    int index = blockIdx.x * blockDim.x + threadIdx.x;
    int stride = blockDim.x * gridDim.x;
    for (int i = index; i < N * N; i += stride) {
        const int m = i / N;
        const int n = i % N;
        DD[n + N * m] = esum<D>(Y + D * n, Y + D * m);
    }
}

template __global__ void vdistance0<2>(const float *, float *, const int);
template __global__ void vdistance0<3>(const float *, float *, const int);

编译器将内联 esum 并展开内部循环,然后它可以使用其重新排序试探法更好地交错加载和触发器以提高吞吐量。生成的代码也具有较低的寄存器占用空间。当我针对 N=10000 和 D=2 运行此程序时,我的速度提高了大约 35%(7.1 毫秒,而在配备 CUDA 9.1 的 GTX 970 上为 4.5 毫秒)。

但是还有比这更明显的优化。您正在执行的计算将产生一个对称的输出矩阵。您只需要执行 (N*N)/2 操作来计算完整矩阵,而不是您在代码中执行的 N*N [技术上 N(N/2 -1) 因为对角线项为零,但为了讨论的目的让我们忘记对角线]。

所以采用不同的方法并使用一个 block 来计算上三角输出矩阵的每一行,然后你可以这样做:

struct udiag
{
    float *p;
    int m;

    __device__ __host__ udiag(float *_p, int _m) : p(_p), m(_m) {};
    __device__ __host__ float* get_row(int i) { return p + (i * (i + 1)) / 2; };
};


template<int D>
__global__ 
void vdistance2(const float * __restrict__ Y, float * __restrict__ DD, const int N)
{
     int rowid = blockIdx.x;
     int colid = threadIdx.x;
     udiag m(DD, N);

     for(; rowid < N; rowid += gridDim.x) {
         float* p = m.get_row(rowid);
         const float* y = Y + D * rowid;
         for(int i=colid; i < (N-rowid); i += blockDim.x) {
             p[i] = esum<D>(y, y + D * i);
         }
    }
}
template __global__ void vdistance2<2>(const float *, float *, const int);
template __global__ void vdistance2<3>(const float *, float *, const int);

这使用了一个小助手类来封装上三角输出矩阵的寻址方案所需的三角数。这样做可以节省大量内存和内存带宽,并减少计算的总 FLOP 计数。如果您之后需要做其他事情,BLAS(和 CUBLAS)支持对上三角矩阵或下三角矩阵进行计算。使用它们。当我运行它时,我获得了大约 75% 的加速(7.1 毫秒对同一 GTX 970 上的 1.6 毫秒)。

重要免责声明:您在此处看到的所有代码都是在 45 分钟的午休期间编写的,并且经过了非常的简单测试。我绝对不声称此答案中的任何内容实际上是正确的。我已经确认它可以编译并且在我运行它以获取分析数据时不会产生运行时错误。这就对了。 Cavaet Emptor 等等。

关于c++ - 为三角矩阵计算优化 CUDA 内核的执行,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48062194/

有关c++ - 为三角矩阵计算优化 CUDA 内核的执行的更多相关文章

  1. ruby-openid:执行发现时未设置@socket - 2

    我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass

  2. ruby-on-rails - 如何优雅地重启 thin + nginx? - 2

    我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server

  3. ruby-on-rails - 使用一系列等级计算字母等级 - 2

    这里是Ruby新手。完成一些练习后碰壁了。练习:计算一系列成绩的字母等级创建一个方法get_grade来接受测试分数数组。数组中的每个分数应介于0和100之间,其中100是最大分数。计算平均分并将字母等级作为字符串返回,即“A”、“B”、“C”、“D”、“E”或“F”。我一直返回错误:avg.rb:1:syntaxerror,unexpectedtLBRACK,expecting')'defget_grade([100,90,80])^avg.rb:1:syntaxerror,unexpected')',expecting$end这是我目前所拥有的。我想坚持使用下面的方法或.join,

  4. ruby - Chef 执行非顺序配方 - 2

    我遵循了教程http://gettingstartedwithchef.com/,第1章。我的运行list是"run_list":["recipe[apt]","recipe[phpap]"]我的phpapRecipe默认Recipeinclude_recipe"apache2"include_recipe"build-essential"include_recipe"openssl"include_recipe"mysql::client"include_recipe"mysql::server"include_recipe"php"include_recipe"php::modul

  5. ruby - 为什么 Ruby 的 each 迭代器先执行? - 2

    我在用Ruby执行简单任务时遇到了一件奇怪的事情。我只想用每个方法迭代字母表,但迭代在执行中先进行:alfawit=("a".."z")puts"That'sanalphabet:\n\n#{alfawit.each{|litera|putslitera}}"这段代码的结果是:(缩写)abc⋮xyzThat'sanalphabet:a..z知道为什么它会这样工作或者我做错了什么吗?提前致谢。 最佳答案 因为您的each调用被插入到在固定字符串之前执行的字符串文字中。此外,each返回一个Enumerable,实际上您甚至打印它。试试

  6. ruby - 检查是否通过 require 执行或导入了 Ruby 程序 - 2

    如何检查Ruby文件是否是通过“require”或“load”导入的,而不是简单地从命令行执行的?例如:foo.rb的内容:puts"Hello"bar.rb的内容require'foo'输出:$./foo.rbHello$./bar.rbHello基本上,我想调用bar.rb以不执行puts调用。 最佳答案 将foo.rb改为:if__FILE__==$0puts"Hello"end检查__FILE__-当前ruby​​文件的名称-与$0-正在运行的脚本的名称。 关于ruby-检查是否

  7. 旋转矩阵的几何意义 - 2

    点向量坐标矩阵的几何意义介绍旋转矩阵的几何含义之前,先介绍一下点向量坐标矩阵的几何含义点:在一维空间下就是一个标量,如同一条直线上,以任意某一个位置为0点,以一定的尺度间隔为1,2,3...,相反方向为-1,-2,-3...;如此就形成了一维坐标系,这时候任何一个点都可以用一个数值表示,如点p1=5,即即从原点出发沿着x轴正方向移动5个尺度;点p2=-3,负方向移动3个尺度;     在一维坐标系上过原点做垂直于一维坐标系的直线,则形成了二维坐标系,此时描述一个点需要两个数值来表示点p3=(3,2),即从原点出发沿着x轴正方向移动3个尺度,在此基础上沿着y轴正方向移动两个尺度的位置就是点p3。

  8. ruby - 使用 `+=` 和 `send` 方法 - 2

    如何将send与+=一起使用?a=20;a.send"+=",10undefinedmethod`+='for20:Fixnuma=20;a+=10=>30 最佳答案 恐怕你不能。+=不是方法,而是语法糖。参见http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_expressions.html它说Incommonwithmanyotherlanguages,Rubyhasasyntacticshortcut:a=a+2maybewrittenasa+=2.你能做的最好的事情是:

  9. postman——集合——执行集合——测试脚本——pm对象简单示例02 - 2

    //1.验证返回状态码是否是200pm.test("Statuscodeis200",function(){pm.response.to.have.status(200);});//2.验证返回body内是否含有某个值pm.test("Bodymatchesstring",function(){pm.expect(pm.response.text()).to.include("string_you_want_to_search");});//3.验证某个返回值是否是100pm.test("Yourtestname",function(){varjsonData=pm.response.json

  10. ruby-on-rails - rbenv:从 RVM 移动到 rbenv 后,在 Jenkins 执行 shell 中找不到命令 - 2

    我从Ubuntu服务器上的RVM转移到rbenv。当我使用RVM时,使用bundle没有问题。转移到rbenv后,我在Jenkins的执行shell中收到“找不到命令”错误。我内爆并删除了RVM,并从~/.bashrc'中删除了所有与RVM相关的行。使用后我仍然收到此错误:rvmimploderm~/.rvm-rfrm~/.rvmrcgeminstallbundlerecho'exportPATH="$HOME/.rbenv/bin:$PATH"'>>~/.bashrcecho'eval"$(rbenvinit-)"'>>~/.bashrc.~/.bashrcrbenvversions

随机推荐