【问题标题】:thrust::max_element slow in comparison cublasIsamax - More efficient implementation?与 cublasIsamax 相比,thrust::max_element 慢 - 更有效的实现?
【发布时间】:2015-03-11 15:15:01
【问题描述】:

我需要一个快速有效的实现来查找 CUDA 中数组中最大值的索引。此操作需要执行多次。我最初为此使用了 cublasIsamax,但是,遗憾的是,它返回了最大绝对值的索引,这不是我想要的。相反,我使用的是thrust::max_element,但是与cublasIsamax 相比速度相当慢。我以以下方式使用它:

//d_vector is a pointer on the device pointing to the beginning of the vector, containing nrElements floats.
thrust::device_ptr<float> d_ptr = thrust::device_pointer_cast(d_vector);
thrust::device_vector<float>::iterator d_it = thrust::max_element(d_ptr, d_ptr + nrElements);
max_index = d_it - (thrust::device_vector<float>::iterator)d_ptr;

向量中元素的数量在 10'000 到 20'000 之间。推力::max_element 和 cublasIsamax 之间的速度差异相当大。也许我在不知不觉中执行了几次内存事务?

【问题讨论】:

  • 如果你计算 x+abs(x) 和 x-abs(x) 的最大值,然后在它们之间进行选择?
  • 我不太明白你的意思。
  • 抱歉这么短。 @RobertCrovella 已经令人满意地回答了您的问题,因此没有什么可以添加到他的自定义 CUDA 内核中。我只是建议一种利用cublasIsamax 来查找数组的max 的方法。如果定义两个新数组ypyn,其元素分别为yp[n] = 0.5f * (x[n] + abs(x[n]))yn[n] = 0.5f * (x[n] - abs(x[n])),则yp 将仅包含x 的正元素,而0 将代替负元素元素,而yn 将只包含x 的负面元素,而0 将代替正面元素。
  • 然后您可以将cublasIsamax 应用到ypcublasIsaminyn。如果yp &gt; 0max,那么您确定它是xmax。否则你得看ynmin
  • 或者,应用expf 并使用cublasIsamax

标签: c++ performance cuda thrust cublas


【解决方案1】:

更有效的实现是在 CUDA 中编写您自己的最大索引缩减代码。 cublasIsamax 很可能在后台使用了类似的东西。

我们可以比较 3 种方法:

  1. thrust::max_element
  2. cublasIsamax
  3. 自定义 CUDA 内核

这是一个完整的例子:

$ cat t665.cu
#include <cublas_v2.h>
#include <thrust/extrema.h>
#include <thrust/device_ptr.h>
#include <thrust/device_vector.h>
#include <iostream>
#include <stdlib.h>

#define DSIZE 10000
// nTPB should be a power-of-2
#define nTPB 256
#define MAX_KERNEL_BLOCKS 30
#define MAX_BLOCKS ((DSIZE/nTPB)+1)
#define MIN(a,b) ((a>b)?b:a)
#define FLOAT_MIN -1.0f

#include <time.h>
#include <sys/time.h>

unsigned long long dtime_usec(unsigned long long prev){
#define USECPSEC 1000000ULL
  timeval tv1;
  gettimeofday(&tv1,0);
  return ((tv1.tv_sec * USECPSEC)+tv1.tv_usec) - prev;
}

__device__ volatile float blk_vals[MAX_BLOCKS];
__device__ volatile int   blk_idxs[MAX_BLOCKS];
__device__ int   blk_num = 0;

template <typename T>
__global__ void max_idx_kernel(const T *data, const int dsize, int *result){

  __shared__ volatile T   vals[nTPB];
  __shared__ volatile int idxs[nTPB];
  __shared__ volatile int last_block;
  int idx = threadIdx.x+blockDim.x*blockIdx.x;
  last_block = 0;
  T   my_val = FLOAT_MIN;
  int my_idx = -1;
  // sweep from global memory
  while (idx < dsize){
    if (data[idx] > my_val) {my_val = data[idx]; my_idx = idx;}
    idx += blockDim.x*gridDim.x;}
  // populate shared memory
  vals[threadIdx.x] = my_val;
  idxs[threadIdx.x] = my_idx;
  __syncthreads();
  // sweep in shared memory
  for (int i = (nTPB>>1); i > 0; i>>=1){
    if (threadIdx.x < i)
      if (vals[threadIdx.x] < vals[threadIdx.x + i]) {vals[threadIdx.x] = vals[threadIdx.x+i]; idxs[threadIdx.x] = idxs[threadIdx.x+i]; }
    __syncthreads();}
  // perform block-level reduction
  if (!threadIdx.x){
    blk_vals[blockIdx.x] = vals[0];
    blk_idxs[blockIdx.x] = idxs[0];
    if (atomicAdd(&blk_num, 1) == gridDim.x - 1) // then I am the last block
      last_block = 1;}
  __syncthreads();
  if (last_block){
    idx = threadIdx.x;
    my_val = FLOAT_MIN;
    my_idx = -1;
    while (idx < gridDim.x){
      if (blk_vals[idx] > my_val) {my_val = blk_vals[idx]; my_idx = blk_idxs[idx]; }
      idx += blockDim.x;}
  // populate shared memory
    vals[threadIdx.x] = my_val;
    idxs[threadIdx.x] = my_idx;
    __syncthreads();
  // sweep in shared memory
    for (int i = (nTPB>>1); i > 0; i>>=1){
      if (threadIdx.x < i)
        if (vals[threadIdx.x] < vals[threadIdx.x + i]) {vals[threadIdx.x] = vals[threadIdx.x+i]; idxs[threadIdx.x] = idxs[threadIdx.x+i]; }
      __syncthreads();}
    if (!threadIdx.x)
      *result = idxs[0];
    }
}

int main(){

  int nrElements = DSIZE;
  float *d_vector, *h_vector;
  h_vector = new float[DSIZE];
  for (int i = 0; i < DSIZE; i++) h_vector[i] = rand()/(float)RAND_MAX;
  h_vector[10] = 10;  // create definite max element
  cublasHandle_t my_handle;
  cublasStatus_t my_status = cublasCreate(&my_handle);
  cudaMalloc(&d_vector, DSIZE*sizeof(float));
  cudaMemcpy(d_vector, h_vector, DSIZE*sizeof(float), cudaMemcpyHostToDevice);
  int max_index = 0;
  unsigned long long dtime = dtime_usec(0);
  //d_vector is a pointer on the device pointing to the beginning of the vector, containing nrElements floats.
  thrust::device_ptr<float> d_ptr = thrust::device_pointer_cast(d_vector);
  thrust::device_vector<float>::iterator d_it = thrust::max_element(d_ptr, d_ptr + nrElements);
  max_index = d_it - (thrust::device_vector<float>::iterator)d_ptr;
  cudaDeviceSynchronize();
  dtime = dtime_usec(dtime);
  std::cout << "thrust time: " << dtime/(float)USECPSEC << " max index: " << max_index << std::endl;
  max_index = 0;
  dtime = dtime_usec(0);
  my_status = cublasIsamax(my_handle, DSIZE, d_vector, 1, &max_index);
  cudaDeviceSynchronize();
  dtime = dtime_usec(dtime);
  std::cout << "cublas time: " << dtime/(float)USECPSEC << " max index: " << max_index << std::endl;
  max_index = 0;
  int *d_max_index;
  cudaMalloc(&d_max_index, sizeof(int));
  dtime = dtime_usec(0);
  max_idx_kernel<<<MIN(MAX_KERNEL_BLOCKS, ((DSIZE+nTPB-1)/nTPB)), nTPB>>>(d_vector, DSIZE, d_max_index);
  cudaMemcpy(&max_index, d_max_index, sizeof(int), cudaMemcpyDeviceToHost);
  dtime = dtime_usec(dtime);
  std::cout << "kernel time: " << dtime/(float)USECPSEC << " max index: " << max_index << std::endl;


  return 0;
}
$ nvcc -O3 -arch=sm_20 -o t665 t665.cu -lcublas
$ ./t665
thrust time: 0.00075 max index: 10
cublas time: 6.3e-05 max index: 11
kernel time: 2.5e-05 max index: 10
$

注意事项:

  1. CUBLAS 返回的索引比其他索引高 1,因为 CUBLAS uses 1-based indexing
  2. CUBLAS might be quicker 如果您使用了CUBLAS_POINTER_MODE_DEVICE,但是为了验证,您仍然需要将结果复制回主机。
  3. 带有CUBLAS_POINTER_MODE_DEVICE 的CUBLAS 应该是异步的,所以cudaDeviceSynchronize() 对于我在这里展示的基于主机的时序是可取的。在某些情况下,推力也可以是异步的。
  4. 为了方便和比较 CUBLAS 和其他方法之间的结果,我将所有非负值用于我的数据。如果您也使用负值,则可能需要调整 FLOAT_MIN 值。
  5. 如果您对性能有疑虑,可以尝试调整nTPBMAX_KERNEL_BLOCKS 参数,看看是否可以在特定GPU 上发挥最大性能。内核代码还可以说通过在(两个)线程块减少的最后阶段不小心切换到扭曲同步模式而留下了一些性能。
  6. 线程块缩减内核使用块耗尽/最后​​块策略来避免额外内核启动以执行最终缩减的开销。

【讨论】:

  • 哇,我没想到会有自定义内核。谢谢!为什么 cublas 比自定义内核慢?我对 CUDA 不是很精通,所以我需要一些时间才能理解您所写的内容。如果我对您的代码有疑问,我会回复您,如果您觉得可以的话。但是快速浏览一下,如果我想包含负数,我将 FLOAT_MIN 初始化为 std::numeric_limits::min();?
  • 是的,只要使FLOAT_MIN 的负数比您预期的最大负数还多。但是,我不确定您是否可以直接使用std::numeric_limits&lt;float&gt;::min()。 (你可以试试。)CUBLAS 可能比自定义内核慢,因为它还执行abs() 函数(或类似的东西,例如平方值),而自定义内核不是。我真的不知道 CUBLAS 函数里面是什么,只是猜测。
猜你喜欢
  • 1970-01-01
  • 2019-11-19
  • 2013-02-22
  • 2015-07-17
  • 2012-03-21
  • 2014-04-30
  • 2013-08-04
  • 2021-07-09
相关资源
最近更新 更多