Parallel processing 并行交叉积

Parallel processing 并行交叉积,parallel-processing,cuda,gpu,Parallel Processing,Cuda,Gpu,免责声明:我对CUDA和并行编程相当陌生——因此,如果您不想费心回答我的问题,请忽略这一点,或者至少向我指出正确的资源,以便我自己找到答案 这是我想用并行编程解决的一个特殊问题。我有一些1D数组以这种格式存储3D向量->[v0x,v0y,v0z,…vnx,vny,vnz],其中n是向量,x,y,z是各自的组件 假设我想找到一个数组中向量[v0,v1,…vn]与另一个数组中相应向量[v0,v1,…vn]之间的叉积 没有并行化,计算非常简单: result[x] = vec1[y]*vec2[z]

免责声明:我对CUDA和并行编程相当陌生——因此,如果您不想费心回答我的问题,请忽略这一点,或者至少向我指出正确的资源,以便我自己找到答案

这是我想用并行编程解决的一个特殊问题。我有一些1D数组以这种格式存储3D向量->
[v0x,v0y,v0z,…vnx,vny,vnz]
,其中
n
是向量,
x
y
z
是各自的组件

假设我想找到一个数组中向量
[v0,v1,…vn]
与另一个数组中相应向量
[v0,v1,…vn]
之间的叉积

没有并行化,计算非常简单:

result[x] = vec1[y]*vec2[z] - vec1[z]*vec2[y];

result[y] = vec1[z]*vec2[x] - vec1[x]*vec2[z];

result[z] = vec1[x]*vec2[y] - vec1[y]*vec2[x];

我面临的问题是理解如何为我目前拥有的阵列实现CUDA并行化。由于结果向量中的每个值都是单独的计算,因此我可以有效地对每个向量并行运行上述计算。由于生成的叉积的每个组成部分都是一个单独的计算,因此它们也可以并行运行。我该如何设置块和线程/考虑为这样的问题设置线程?

对于任何CUDA程序员来说,最重要的两个优化优先级是高效使用内存,并公开足够的并行性以隐藏延迟。我们将用这些来指导我们的算法选择

在任何转换(与还原相反)类型的问题中,一个非常简单的线程策略(线程策略回答“每个线程将做什么或负责什么?”)是让每个线程负责一个输出值。您的问题符合转换的描述-输出数据集大小与输入数据集大小的顺序相同

我假设你想要有两个相等长度的向量包含你的3D向量,并且你想要取每个向量中第一个3D向量和第二个3D向量的叉积,依此类推

如果我们选择每个线程1个输出点的线程策略(即
result[x]
result[y]
result[z]
,总共将是3个输出点),那么我们将需要3个线程来计算每个向量叉积的输出。如果我们有足够的向量进行乘法,那么我们将有足够的线程使我们的机器“忙碌”,并很好地隐藏延迟。根据经验,如果线程数为10000或更多,您的问题将在GPU上开始变得有趣,因此这意味着我们希望您的1D向量由大约3000个或更多的3D向量组成。让我们假设情况就是这样

为了实现内存效率目标,我们的第一个任务是从全局内存加载向量数据。理想情况下,我们希望将其合并,这大致意味着相邻线程访问内存中的相邻元素。我们希望输出存储也合并在一起,我们的线程策略(每个线程选择一个输出点/一个向量组件)将很好地支持这一点

为了有效地使用内存,我们希望理想情况下只从全局内存加载一次每个项。您的算法自然涉及少量的数据重用。数据重用是显而易见的,因为
result[y]
的计算取决于
vec2[z]
,而
result[x]
的计算也取决于
vec2[z]
仅选取一个示例。因此,当存在数据重用时,典型的策略是首先将数据加载到CUDA共享内存中,然后允许线程基于共享内存中的数据执行其计算。正如我们将看到的,这使得我们可以相当容易/方便地安排来自全局内存的合并负载,因为全局数据负载安排不再与线程或用于计算的数据的使用紧密耦合

最后一个挑战是找出一个索引模式,以便每个线程从共享内存中选择适当的元素相乘。如果我们查看您在问题中描述的计算模式,我们会发现
vec1
的第一个加载遵循计算结果的索引+1(模3)的偏移模式。所以
x
->
y
y
->
z
,和
z
->
x
。同样,我们看到一个+2(模3)模式用于
vec2
的下一次加载,另一个+2(模3)模式用于
vec1
的下一次加载,另一个+1(模3)模式用于
vec2
的最终加载

如果我们将所有这些想法结合起来,那么我们就可以编写一个内核,它应该具有一般高效的特性:

$ cat t1003.cu
#include <stdio.h>

#define TV1 1
#define TV2 2
const size_t N = 4096;    // number of 3D vectors
const int blksize = 192;  // choose as multiple of 3 and 32, and less than 1024
typedef float mytype;
//pairwise vector cross product
template <typename T>
__global__ void vcp(const T * __restrict__ vec1, const T * __restrict__ vec2, T * __restrict__ res, const size_t n){

  __shared__ T sv1[blksize];
  __shared__ T sv2[blksize];
  size_t idx = threadIdx.x+blockDim.x*blockIdx.x;
  while (idx < 3*n){ // grid-stride loop
    // load shared memory using coalesced pattern to global memory
    sv1[threadIdx.x] = vec1[idx];
    sv2[threadIdx.x] = vec2[idx];
    // compute modulo/offset indexing for thread loads of shared data from vec1, vec2
    int my_mod = threadIdx.x%3;   // costly, but possibly hidden by global load latency
    int off1 = my_mod+1;
    if (off1 > 2) off1 -= 3;
    int off2 = my_mod+2;
    if (off2 > 2) off2 -= 3;
    __syncthreads();
    // each thread loads its computation elements from shared memory
    T t1 = sv1[threadIdx.x-my_mod+off1];
    T t2 = sv2[threadIdx.x-my_mod+off2];
    T t3 = sv1[threadIdx.x-my_mod+off2];
    T t4 = sv2[threadIdx.x-my_mod+off1];
    // compute result, and store using coalesced pattern, to global memory
    res[idx] = t1*t2-t3*t4;
    idx += gridDim.x*blockDim.x;}  // for grid-stride loop
}

int main(){

  mytype *h_v1, *h_v2, *d_v1, *d_v2, *h_res, *d_res;
  h_v1  = (mytype *)malloc(N*3*sizeof(mytype));
  h_v2  = (mytype *)malloc(N*3*sizeof(mytype));
  h_res = (mytype *)malloc(N*3*sizeof(mytype));
  cudaMalloc(&d_v1,  N*3*sizeof(mytype));
  cudaMalloc(&d_v2,  N*3*sizeof(mytype));
  cudaMalloc(&d_res, N*3*sizeof(mytype));
  for (int i = 0; i<N; i++){
    h_v1[3*i]    = TV1;
    h_v1[3*i+1]  = 0;
    h_v1[3*i+2]  = 0;
    h_v2[3*i]    = 0;
    h_v2[3*i+1]  = TV2;
    h_v2[3*i+2]  = 0;
    h_res[3*i]   = 0;
    h_res[3*i+1] = 0;
    h_res[3*i+2] = 0;}
  cudaMemcpy(d_v1, h_v1, N*3*sizeof(mytype), cudaMemcpyHostToDevice);
  cudaMemcpy(d_v2, h_v2, N*3*sizeof(mytype), cudaMemcpyHostToDevice);
  vcp<<<(N*3+blksize-1)/blksize, blksize>>>(d_v1, d_v2, d_res, N);
  cudaMemcpy(h_res, d_res, N*3*sizeof(mytype), cudaMemcpyDeviceToHost);
  // verification
  for (int i = 0; i < N; i++) if ((h_res[3*i] != 0) || (h_res[3*i+1] != 0) || (h_res[3*i+2] != TV1*TV2)) { printf("mismatch at %d, was: %f, %f, %f, should be: %f, %f, %f\n", i, h_res[3*i], h_res[3*i+1], h_res[3*i+2], (float)0, (float)0, (float)(TV1*TV2)); return -1;}
  printf("%s\n", cudaGetErrorString(cudaGetLastError()));
  return 0;
}


$ nvcc t1003.cu -o t1003
$ cuda-memcheck ./t1003
========= CUDA-MEMCHECK
no error
========= ERROR SUMMARY: 0 errors
$

内核需要9.8240us来执行,在此期间加载或存储的数据总量为40960*3*4*3字节。因此,内核实现的内存带宽为40960*3*4*3/0.000009824或150 GB/s。此GPU上可达到的峰值代理测量值为171 GB/s,因此此内核可达到最佳吞吐量的88%。通过更仔细的基准测试,连续运行内核两次,第二次执行只需要8.99us即可执行。在这种情况下,实现的带宽将达到峰值可实现吞吐量的96%。

对于任何CUDA程序员来说,最重要的两个优化优先级是高效使用内存,并公开足够的并行性以隐藏延迟。我们将用这些来指导我们的算法选择

在任何转换(与还原相反)类型的问题中,一个非常简单的线程策略(线程策略回答“每个线程将做什么或负责什么?”)是让每个线程负责一个输出值。您的问题符合转换的描述-输出数据集大小与输入数据集大小的顺序相同

我假设你想要两个长度相等的向量来包含你的3D图像
$ CUDA_VISIBLE_DEVICES="1" nvprof ./t1003
==27861== NVPROF is profiling process 27861, command: ./t1003
no error
==27861== Profiling application: ./t1003
==27861== Profiling result:
            Type  Time(%)      Time     Calls       Avg       Min       Max  Name
 GPU activities:   65.97%  162.22us         2  81.109us  77.733us  84.485us  [CUDA memcpy HtoD]
                   30.04%  73.860us         1  73.860us  73.860us  73.860us  [CUDA memcpy DtoH]
                    4.00%  9.8240us         1  9.8240us  9.8240us  9.8240us  void vcp<float>(float const *, float const *, float*, unsigned long)
      API calls:   99.10%  249.79ms         3  83.263ms  6.8890us  249.52ms  cudaMalloc
                    0.46%  1.1518ms        96  11.998us     374ns  454.09us  cuDeviceGetAttribute
                    0.25%  640.18us         3  213.39us  186.99us  229.86us  cudaMemcpy
                    0.10%  255.00us         1  255.00us  255.00us  255.00us  cuDeviceTotalMem
                    0.05%  133.16us         1  133.16us  133.16us  133.16us  cuDeviceGetName
                    0.03%  71.903us         1  71.903us  71.903us  71.903us  cudaLaunchKernel
                    0.01%  15.156us         1  15.156us  15.156us  15.156us  cuDeviceGetPCIBusId
                    0.00%  7.0920us         3  2.3640us     711ns  4.6520us  cuDeviceGetCount
                    0.00%  2.7780us         2  1.3890us     612ns  2.1660us  cuDeviceGet
                    0.00%  1.9670us         1  1.9670us  1.9670us  1.9670us  cudaGetLastError
                    0.00%     361ns         1     361ns     361ns     361ns  cudaGetErrorString
$ CUDA_VISIBLE_DEVICES="1" /usr/local/cuda/samples/bin/x86_64/linux/release/bandwidthTest
[CUDA Bandwidth Test] - Starting...
Running on...

 Device 0: Tesla K20Xm
 Quick Mode

 Host to Device Bandwidth, 1 Device(s)
 PINNED Memory Transfers
   Transfer Size (Bytes)        Bandwidth(MB/s)
   33554432                     6375.8

 Device to Host Bandwidth, 1 Device(s)
 PINNED Memory Transfers
   Transfer Size (Bytes)        Bandwidth(MB/s)
   33554432                     6554.3

 Device to Device Bandwidth, 1 Device(s)
 PINNED Memory Transfers
   Transfer Size (Bytes)        Bandwidth(MB/s)
   33554432                     171220.3

Result = PASS

NOTE: The CUDA Samples are not meant for performance measurements. Results may vary when GPU Boost is enabled.
$