用于处理CUDA中的4D张量的内核

时间:2015-12-30 12:15:51

标签: c++ cuda gpgpu

我想编写一个内核来执行依赖于索引的所有唯一四元组(ij | kl)的计算。生成主机上所有唯一四重奏的代码如下:

<%@ Page Title="" Language="C#" MasterPageFile="~/MasterPage.master" AutoEventWireup="true" CodeFile="Donor.aspx.cs" Inherits="Donor" %>

<asp:Content ID="Content1" ContentPlaceHolderID="head" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
    <asp:GridView ID="GridView1" runat="server"  
         AutoGenerateColumns="False" CellPadding="4" 
         DataKeyNames="Id" DataSourceID="SqlDataSource1" 
         EmptyDataText="There are no data records to display." 
         ForeColor="#333333" GridLines="None" AllowSorting="True">
        <AlternatingRowStyle BackColor="White" />
        <Columns>
            <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" ItemStyle-Width="120px"/>
            <asp:BoundField DataField="Name" HeaderText="Name" SortExpression="Name"  ItemStyle-Width="150px"/>
            <asp:BoundField DataField="BloodGroup" HeaderText="BloodGroup" SortExpression="BloodGroup"  ItemStyle-Width="120px"/>
            <asp:BoundField DataField="Disease" HeaderText="Disease" SortExpression="Disease"  ItemStyle-Width="120px"/>
        </Columns>
        <FooterStyle BackColor="#990000" Font-Bold="True" ForeColor="White" />
        <HeaderStyle BackColor="#990000" Font-Bold="True" ForeColor="White" />
        <PagerStyle BackColor="#FFCC66" ForeColor="#333333" HorizontalAlign="Center" />
        <RowStyle BackColor="#FFFBD6" ForeColor="#333333" />
        <SelectedRowStyle BackColor="#FFCC66" Font-Bold="True" ForeColor="Navy" />
        <SortedAscendingCellStyle BackColor="#FDF5AC" />
        <SortedAscendingHeaderStyle BackColor="#4D0000" />
        <SortedDescendingCellStyle BackColor="#FCF6C0" />
        <SortedDescendingHeaderStyle BackColor="#820000" />
    </asp:GridView>
    <asp:SqlDataSource ID="SqlDataSource1" runat="server" 
         ConnectionString="<%$ ConnectionStrings:ConnectionString %>" 
         SelectCommand="SELECT [Name], [BloodGroup], [Disease], [Id] FROM [Patient]">
    </asp:SqlDataSource>
    <asp:DetailsView ID="DetailsView1" runat="server" CellPadding="4" 
         DataSourceID="SqlDataSource2" ForeColor="#333333" 
         GridLines="None" Height="50px" Width="125px" 
         DefaultMode="Insert">
        <AlternatingRowStyle BackColor="White" />
        <CommandRowStyle BackColor="#FFFFC0" Font-Bold="True" />
        <FieldHeaderStyle BackColor="#FFFF99" Font-Bold="True" />
        <Fields>
            <asp:CommandField ShowInsertButton="True" />
        </Fields>
        <FooterStyle BackColor="#990000" Font-Bold="True" ForeColor="White" />
        <HeaderStyle BackColor="#990000" Font-Bold="True" ForeColor="White" />
        <PagerStyle BackColor="#FFCC66" ForeColor="#333333" HorizontalAlign="Center" />
        <RowStyle BackColor="#FFFBD6" ForeColor="#333333" />
    </asp:DetailsView>
    <asp:SqlDataSource ID="SqlDataSource2" runat="server" ConnectionString="<%$ ConnectionStrings:ConnectionString %>" SelectCommand="SELECT [Name], [BloodGroup], [Disease] FROM [Patient]" InsertCommand="INSERT INTO [Patient] ([Name],[BloodGroup],[Disease]) VALUES (@Name, @BloodGroup, @Disease)">
        <InsertParameters>
            <asp:Parameter Name="Name" />
            <asp:Parameter Name="BloodGroup" />
            <asp:Parameter Name="Disease" />
        </InsertParameters>
    </asp:SqlDataSource>
</asp:Content>

输出:

#include <iostream>


using namespace std;

int main(int argc, char** argv)
{
    unsigned int i,j,k,l,ltop;
    unsigned int nao=7;
    for(i=0;i<nao;i++)
    {
        for(j=0;j<=i;j++)
        {
            for(k=0;k<=i;k++)
            {
                ltop=k;
                if(i==k)
                {
                    ltop=j;
                }
                for(l=0;l<=ltop; l++)
                {
                    printf("computing the ERI (%d,%d|%d,%d) \n",i,j,k,l);
                }
            }    
        }
    }

    int m = nao*(nao+1)/2;
    int eris_size = m*(m+1)/2;
    cout<<"The total size of the sack of ERIs is: "<<eris_size<<endl;

    return 0;
}

我想使用CUDA内核恢复同一组四重奏,但我无法得到它。我现在拥有的CUDA代码如下

computing the ERI (0,0|0,0) 
computing the ERI (1,0|0,0) 
computing the ERI (1,0|1,0) 
computing the ERI (1,1|0,0) 
computing the ERI (1,1|1,0) 
computing the ERI (1,1|1,1) 
computing the ERI (2,0|0,0) 
computing the ERI (2,0|1,0) 
computing the ERI (2,0|1,1) 
computing the ERI (2,0|2,0) 
computing the ERI (2,1|0,0) 
computing the ERI (2,1|1,0) 
computing the ERI (2,1|1,1) 
computing the ERI (2,1|2,0) 
computing the ERI (2,1|2,1) 
computing the ERI (2,2|0,0) 
computing the ERI (2,2|1,0) 
computing the ERI (2,2|1,1) 
computing the ERI (2,2|2,0) 
computing the ERI (2,2|2,1) 
computing the ERI (2,2|2,2) 
computing the ERI (3,0|0,0) 
computing the ERI (3,0|1,0) 
computing the ERI (3,0|1,1) 
computing the ERI (3,0|2,0) 
computing the ERI (3,0|2,1) 
computing the ERI (3,0|2,2) 
computing the ERI (3,0|3,0) 
computing the ERI (3,1|0,0) 
computing the ERI (3,1|1,0) 
computing the ERI (3,1|1,1) 
computing the ERI (3,1|2,0) 
computing the ERI (3,1|2,1) 
computing the ERI (3,1|2,2) 
computing the ERI (3,1|3,0) 
computing the ERI (3,1|3,1) 
computing the ERI (3,2|0,0) 
computing the ERI (3,2|1,0) 
computing the ERI (3,2|1,1) 
computing the ERI (3,2|2,0) 
computing the ERI (3,2|2,1) 
computing the ERI (3,2|2,2) 
computing the ERI (3,2|3,0) 
computing the ERI (3,2|3,1) 
computing the ERI (3,2|3,2) 
computing the ERI (3,3|0,0) 
computing the ERI (3,3|1,0) 
computing the ERI (3,3|1,1) 
computing the ERI (3,3|2,0) 
computing the ERI (3,3|2,1) 
computing the ERI (3,3|2,2) 
computing the ERI (3,3|3,0) 
computing the ERI (3,3|3,1) 
computing the ERI (3,3|3,2) 
computing the ERI (3,3|3,3)

输出:

#include <iostream>
#include <stdio.h>

using namespace std;

#define ABS(x)  (x<0)?-x:x

__global__
void test_kernel(int basis_size)
{
    unsigned int i_idx = threadIdx.x + blockIdx.x * blockDim.x;
    unsigned int j_idx = threadIdx.y + blockIdx.y * blockDim.y;


    // Building the quartets                                                                                                                                                  

    unsigned int i_orbital, j_orbital, k_orbital, l_orbital, i__1,j__1;
    unsigned int i_primitive, j_primitive, k_primitive, l_primitive;

    i_orbital = (i_idx + 1)%basis_size /*+1*/;
    j_orbital = (i__1 = (i_idx) / basis_size, ABS(i__1)) /*+ 1*/;

    k_orbital = (j_idx+1)%basis_size /*+1*/;
    l_orbital = (j__1 = (j_idx) / basis_size, ABS(j__1)) /*+ 1*/;
    unsigned int ltop;
    ltop=k_orbital;
    if(i_orbital==k_orbital)
    {
        ltop=j_orbital;
    }
    if(i_orbital<basis_size && j_orbital<=i_orbital && k_orbital<=i_orbital && l_orbital<=ltop)
        printf("computing the ERI (%d,%d|%d,%d)   \n", i_orbital, j_orbital,k_orbital,l_orbital);
}

int main(int argc, char *argv[])
{
    int nao = 7;
    cudaDeviceReset();
    /* partitioning from blocks to grids */
    int dimx = 8;
    int dimy = 8;
    dim3 block(dimx, dimy);   // we will try blocks of 8x8 threads                                                                                                            
    dim3 grid((nao+block.x-1)/block.x, (nao+block.y-1)/block.y);   // The grids are shaped accordingly                                                                        

    /* Launch the kernel */
    test_kernel<<<grid,block>>>(nao);
    cudaDeviceReset();

    return 0;
}

四重奏将驱动排斥积分的计算,其输入参数存储在大小为nao且为3的数组中

1 个答案:

答案 0 :(得分:3)

如上所述,您的代码并没有多做&#34;。它生成索引并打印出来。此外,还不清楚nao=7是否实际描述了您的最终问题规模,或者它是否仅用于演示目的。

纯粹从指数生成的角度来看,有很多方法可以解决您的问题。其中一些可能自然有助于GPU的有效使用,有些可能不会。但是,从目前为止所显示的代码中无法确定GPU的有效使用情况。例如,CUDA程序的一个理想目标是从全局存储器合并访问。由于您的代码没有显示,因此在不知道生成的访问模式可能是什么的情况下提出解决方案有点冒险。

因此,真正的问题&#34>可能会出现问题。在做出关于索引生成的最终决定之前,你试图解决可能会有一些考虑因素(这实际上等于&#34;将工作分配给给定的线程&#34;)。我会尝试提及其中一些。

对您的代码的一些批评:

  1. 您提出的实现建议使用2D线程块和网格结构来有效地提供程序使用的i,j索引。但是,通过检查原始C源代码,我们看到j索引只覆盖了一个直到i索引的空间:

    for(i=0;i<nao;i++)
    {
        for(j=0;j<=i;j++)
    

    但是,使用2D网格/线程块结构替换此类结构会创建一个矩形空间,而原始C代码中的嵌套for循环并不暗示这一空间。因此,发射这样的空间/网格将导致创建超出范围的i,j指数;那些线程不需要工作。我们可以解决这个问题(也许这就是你的代码试图做的事情)&#34; re-purposeing&#34;超出范围的线程来覆盖一些额外的空间,但这导致相当复杂和难以阅读的算术,并看到下一个批评。

  2. 你的内核没有循环,所以很明显每个线程只能生成一行打印输出。因此,每个线程只能负责一个ERI条目。根据您提供的C源代码,对于nao大小为7,您需要406个ERI条目(对于您显示的代码,BTW,您发布的打印输出似乎不完整)。如果我们每个线程有一个打印输出,并且我们需要覆盖406个ERI条目的空间,我们最好至少有406个线程,或者我们提出的解决方案根本无法工作。如果我们检查您的代码以根据线程确定网格的大小:

    int dimx = 8;
    int dimy = 8;
    dim3 block(dimx, dimy);   // we will try blocks of 8x8 threads                                                                                                            
    dim3 grid((nao+block.x-1)/block.x, (nao+block.y-1)/block.y);   // The grids are shaped accordingly                                                                        
    

    我们将得出结论,你根本就没有启动足够的线程。如果你完成上述计算(你可能想打印出block.x,.y和grid.x,.y值,如果不确定),你会发现你正在启动一个 8x8线程块,即总共64个线程,对于nao的7. {64}个线程,每个线程一个打印输出,不能覆盖406个条目的ERI空间。事实上,鉴于您的printfif语句的限制,可能每个帖子的打印输出少于一个。

  3. 可能的解决方案想法:

    1. 一种相当简单的方法就是将C源代码中的外部两个for循环映射到2D网格的x,y索引。然后,我们将保留或多或少完整的C源代码的内部for循环结构,作为我们的内核代码。这很容易写。它的缺点是会启动一些不会做任何事情的线程(我们必须检查j&gt; i的情况并判断这些线程什么也不做),但这可能是一个小问题。更大的问题可能是我们生成了多少实际线程。对于给定的nao 7,这将启动7x7 = 49个线程(其中一些不起作用)。对于GPU来说,问题大小为49,甚至406个线程很小,并且由于线程数量有限,它将无法实现峰值性能。 &#34;三角形&#34;这个问题的本质(j&lt; = i)意味着这种方法的进一步改进是启动一个线性线程块,然后使用linear-to-triangular index mapping来表示线性线程块索引中的i,j,这将导致no& #34;浪费&#34;线程。然而,正如已经讨论的那样,一个比约50%浪费的线程更重要的考虑因素是全球访问的合并。

    2. 另一种可能的方法是使用@AngryLettuce建议的方法评论您之前的一个问题(现在可能已删除)。具体地,生成1D网格和1D全局唯一线程索引,然后使用算法计算必要的子索引(i,j,k,l)以将1D索引转换为子索引。这样做的优点是,对于小问题,我们可以直接启动更大的内核。例如,对于406个ERI条目的问题空间,我们将生成(至少)406个线程,其中数字唯一索引为0..405,并将该1-D索引转换为4个子索引(i,j,k, L)。查看您的内核代码,这可能就是您要做的事情。但是,由于你的空间形状奇特,我认为从线性索引(或任何矩形指数集)转换为奇形状空间的算法会很复杂。

    3. 如果您的真实问题空间很大(nao远大于7),那么我个人会选择第一种方法,因为它将是更易读的代码(IMO)并且易于编写/维护。对于小问题空间,由于已经讨论过的原因,第二种方法可能更好。对于上述任何一种方法,我们都希望研究生成的全局内存访问模式。这取决于您尚未展示的数据组织。第一种方法可能更容易编写以生成合并访问,但在第二种方法中仔细使用索引生成算法应该(理论上)允许您实现您希望的任何访问模式。

      这是方法1的完整代码:

      #include <stdio.h>
      #include <iostream>
      
      using namespace std;
      
      __global__ void kernel(unsigned int nao)
      {
          unsigned i = threadIdx.x+blockDim.x*blockIdx.x;
          unsigned j = threadIdx.y+blockDim.y*blockIdx.y;
          if (i >= nao) return;
          if (j > i) return; // modify if use of __syncthreads() after this
      
          unsigned int k,l,ltop;
                  for(k=0;k<=i;k++)
                  {
                      ltop=k;
                      if(i==k)
                      {
                          ltop=j;
                      }
                      for(l=0;l<=ltop; l++)
                      {
                          printf("computing the ERI (%d,%d|%d,%d) \n",i,j,k,l);
                      }
                  }
      }
      
      
      int main(){
          unsigned int nao = 7;
          dim3 block(4,4);
          dim3 grid((nao+block.x-1)/block.x, (nao+block.y-1)/block.y);
          kernel<<<grid,block>>>(nao);
          cudaDeviceSynchronize();
          int m = nao*(nao+1)/2;
          int eris_size = m*(m+1)/2;
          cout<<"The total size of the sack of ERIs is: "<<eris_size<<endl;
      
          return 0;
      }
      

      请注意,我在此代码中使用了条件返回语句以便于演示(对于不起作用的线程)。但是,如果需要在内核中使用__syncthreads(),则需要修改内核结构以避免条件返回,而是允许&#34;超出范围&#34;线程在没有工作的情况下继续通过内核代码。关于__syncthreads()用法的许多其他SO问题涵盖了这个概念。还要注意,内核打印输出可以按任何顺序发生(就像线程可以按任何顺序执行一样),所以我只验证了上面的方法似乎工作并产生了所需的406行打印输出。更好的验证方法是避免使用printf并改为使用计数矩阵。

      对于第二种方法,从单个线性索引到多维空间的转换(使用1:1映射,没有浪费/未使用的索引)相当复杂,如前所述,因为您的多维空间是&#34;形状奇特&#34;。我没有&#34;伟大&#34;从线性索引转换为子索引的方法。我必须根据i,j,k索引绘制l循环的各种问题空间维度大小:

      i=0, j=0, k=0, l=0  (total 1 iteration for i=0)
      
      l loop limit, for i=1: (total 5 iterations for i=1)
           j
          0 1
      k 0 0 0
        1 0 1
      
      l loop limit, for i=2: (total 15 iterations for i=2)
           j
          0 1 2
      k 0 0 0 0
        1 1 1 1
        2 0 1 2
      
      l loop limit, for i=3:  (total 34 iterations for i=3)
           j
          0 1 2 3
      k 0 0 0 0 0
        1 1 1 1 1
        2 2 2 2 2
        3 0 1 2 3
      
       etc.
      

      (注意,另一种映射方法是将上面每个矩阵的最后一行视为等于k的常量,就像其他行一样,这会导致一些浪费的线程,但会大大简化索引计算我们将简要讨论无浪费线程方法,但最终使用浪费线程方法提供一个示例,因为索引计算在算术和时间复杂度方面都大大简化了。)

      研究上述内容,我们看到映射到给定i循环迭代的迭代次数为:

      [((i+2)*(i+1)/2)*(i+1)] - (i+1)*i/2
      

      或:

      (((i+2)*(i+1)-i)/2)*(i+1)
      

      上述计算可用于根据线性索引确定i的索引。我所知道的使用上述断点剖析线性索引的唯一方法是通过二进制搜索。

      然后,我们将重复上述方法(确定每个&#34;级别&#34;的迭代次数,并使用二进制搜索将线性索引映射到级别索引)以计算下一个索引j和k。剩余数量将是l指数。

      为简化讨论的其余部分,我已切换到使用修改后的映射版本:

      l loop limit, for i=1: (total 6 iterations for i=1)
           j
          0 1
      k 0 0 0
        1 1 1
      
      l loop limit, for i=2: (total 18 iterations for i=2)
           j
          0 1 2
      k 0 0 0 0
        1 1 1 1
        2 2 2 2
      
      l loop limit, for i=3:  (total 40 iterations for i=3)
           j
          0 1 2 3
      k 0 0 0 0 0
        1 1 1 1 1
        2 2 2 2 2
        3 3 3 3 3
      
       etc.
      

      我们将从中获得一些浪费的线程,我们将在内核中处理if条件。

      上面每个i级的迭代次数只是:

      (i+2)*(i+1)*(i+1)/2
      

      一旦我们从给定线性索引的上述公式计算了i-index(使用summation of the above polynomial上的二进制搜索),那么我们可以通过划分剩余空间来非常容易地计算下一个索引(j)进入我相同大小的部分。可以使用triangular mapping method找到下一个k索引,然后剩余空间成为我们的l循环范围。但是我们必须记住如前所述调整这个l指数。

      这是方法2的完整代码:

      #include <stdio.h>
      #include <iostream>
      
      // the closed-form summation of the polynomial (i+2)*(i+1)*(i+1)/2 from 0 to n
      __host__ __device__ unsigned my_func(unsigned i){ return 1+(2*i+(((i*(i+1))/2)*((i*(i+1))/2)) + (i*(i+1)*((2*i)+1)*2)/3 + ((5*i*(i+1))/2))/2; }
      
      // binary search
      __host__ __device__ unsigned bsearch_functional(const unsigned key, const unsigned len_a, unsigned (*func)(unsigned)){
        unsigned lower = 0;
        unsigned upper = len_a;
        unsigned midpt;
        while (lower < upper){
          midpt = (lower + upper)>>1;
          if (func(midpt) <= key) lower = midpt +1;
          else upper = midpt;
          }
        return lower;
      }
      
      // conversion of linear index to triangular matrix (row,col) index
      __host__ __device__ void linear_to_triangular(const unsigned i, const unsigned n, unsigned *trow, unsigned *tcol)
      {
          int c = i;
          int row = c/(n-1);
          int col = c%(n-1);
      
          if (col < row) {
            col = (n-2)-col;
            row = (n-1)-row;
          }
      
          *tcol = col+1;
          *trow = row;
      }
      
      
      
      __global__ void kernel(unsigned int nao, unsigned int eris_size)
      {
          unsigned idx = threadIdx.x+blockDim.x*blockIdx.x;
          if (idx < eris_size){
          // compute i-index via binary search
          unsigned int i = bsearch_functional(idx, nao, my_func);
          // compute j-index via division of the space by i;
          unsigned int j = (idx==0)?0:(idx-my_func(i-1))/((my_func(i)-my_func(i-1))/(i+1));
          unsigned k,l;
          linear_to_triangular((idx-my_func(i-1)-(j *((my_func(i)-my_func(i-1))/(i+1)))), (i+2), &k, &l);
          k = i-k;
          l = (i+1)-l;
          if (idx == 0) {k=0; l=0;}
          if (l <= ((i==k)?j:k))
            printf("computing the ERI (%d,%d|%d,%d) \n",i,j,k,l);
          }
      }
      
      int main(){
          unsigned int nao = 7;
          int m = nao*(nao+1)/2;
          int eris_size = m*(m+1)/2;
          const int nTPB = 64;
          std::cout<<"The total size of the sack of ERIs is: "<<eris_size<<std::endl;
          int mod_eris_size = my_func(nao-1);
          kernel<<<mod_eris_size+nTPB-1/nTPB, nTPB>>>(nao, mod_eris_size);
          cudaDeviceSynchronize();
          return 0;
      }
      

      要明确的是,我并不确切知道您的任务是什么,我不保证这些示例对于任何给定的用法都没有错误。我的意图不是给你一个黑盒子,而是解释一些可能的编程方法。我没有做过严格的验证,只是观察到每种方法产生了406行ERI输出,与原始C代码相同。

      最后,为了简洁起见,我省略了proper cuda error checking,但是每当您遇到cuda代码时遇到问题,我建议您使用它并使用cuda-memcheck运行代码。