Parallel processing 并行交叉积
免责声明:我对CUDA和并行编程相当陌生——因此,如果您不想费心回答我的问题,请忽略这一点,或者至少向我指出正确的资源,以便我自己找到答案 这是我想用并行编程解决的一个特殊问题。我有一些1D数组以这种格式存储3D向量->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]
[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.
$