Использование тяги медленнее моего собственного ядра? - PullRequest
0 голосов
/ 18 мая 2019

Eidt

Измените код, как предложил Роберт, но тяга все еще намного медленнее.

Данные, которые я использовал, основаны на двух файлах .dat, поэтому я опускаю их в коде.

Исходная задача

У меня есть два сложных вектора, которые были добавлены в GPU Tesla M6. Я хочу вычислить поэлементное произведение двух векторов, а именно [x1 * y1, ..., xN * yN]. Длина двух векторов равна N = 720 896.

Фрагмент кода (измененный)

Я решаю эту проблему двумя способами. Один использует Thrust с преобразованием типов и определенной структурой:

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

#include "cuda_runtime.h"
#include "cuComplex.h"

#include <thrust/host_vector.h>
#include <thrust/device_vector.h>
#include <thrust/execution_policy.h>
#include <thrust/complex.h>
#include <thrust/transform.h>
#include <thrust/functional.h>


using namespace std;

typedef thrust::complex<float> comThr;

// ---- struct for thrust ----//
struct Complex_Mul_Complex :public thrust::binary_function<comThr, comThr, comThr>
{
    __host__ __device__
    comThr operator() (comThr a, comThr b) const{
        return a*b;
    }
};

// ---- my kernel function ---- //
__global__ void HardamarProductOnDeviceCC(cuComplex *Result, cuComplex *A, cuComplex *B, int N)
{
unsigned int tid = threadIdx.x;
unsigned int index = threadIdx.x + blockDim.x * blockIdx.x;

if(index >= N)
    return;
Result[index].x = cuCmul(A[index],B[index]).x;
Result[index].y = cuCmul(A[index],B[index]).y;

}

// ---- timing function ---- //
double seconds()
{
    struct timeval tp;
    struct timezone tzp;
    int i = gettimeofday(&tp, &tzp);
    return ((double)tp.tv_sec + (double)tp.tv_usec * 1.e-6);
}
int main()
{
int N = 720896;
cuComplex *d_Data1, *d_Data2;
cudaMalloc(&d_Data1, N*sizeof(d_Data1[0]));
cudaMalloc(&d_Data2, N*sizeof(d_Data2[0]));
/************************************
 * Version 1: type conversion twice *
 ************************************/
// step 1: type convert (cuComplex->thrust)
comThr *thr_temp1 = reinterpret_cast<comThr*>(d_Data1);
thrust::device_ptr<comThr> thr_d_Data1 = thrust::device_pointer_cast(thr_temp1);

comThr *thr_temp2 = reinterpret_cast<comThr*>(d_Data2);
thrust::device_ptr<comThr> thr_d_Data2 = thrust::device_pointer_cast(thr_temp2);

// step 2: product and timing
Complex_Mul_Complex op_dot;
double iStart = cpuSecond();  // timing class
for(int i=0;i<1000;i++){    // loop 1000 times to get accurate time consumption
  thrust::transform(thrust::device,thr_d_Data1,thr_d_Data1+N,
    thr_d_Data2,thr_d_Data1,op_dot); 
}
cudaDeviceSynchronize();
double iElapse = cpuSecond() - iStart;
cout << "thrust time consume: " << iElapse <<endl;

/************************************************
 * Version 2: dot product using kernel function *
 ************************************************/
int blockSize;
int minGridSize;
int gridSize;
cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSize, HardamarProductOnDeviceCC, 0, 0);

gridSize = (N+blockSize-1)/blockSize;
dim3 grid(gridSize);
dim3 block(blockSize);
iStart = cpuSecond();
for(int i=0;i<1000;i++){
    HardamarProductOnDeviceCC<<<grid,block>>>(d_Data1,d_Data1,d_Data2,N);
}
cudaDeviceSynchronize();
iElapse = cpuSecond() - iStart;
cout << "kernel time consume: " << iElapse <<endl;
}

Result:
thrust time consume: 25.6063
kernel time consume: 2.87929

Мой вопрос

После того, как я добавил cudaDeviceSynchronize(), похоже, версия с надстройкой намного медленнее, чем версия ядра. Существует «золотое правило», которое использует библиотеки вместо написания собственной функции ядра. Но я хочу знать, почему в этой ситуации версия тяги медленнее?

1 Ответ

4 голосов
/ 18 мая 2019

Запуски ядра CUDA являются асинхронными. Это означает, что элемент управления возвращается потоку хоста, чтобы он мог перейти к следующей строке кода после запуска ядра до того, как ядро ​​даже начало выполнять .

Это покрыто многочисленными вопросами здесь по тегу cuda. Это распространенная ошибка при синхронизации кода CUDA. Это может повлиять на то, как вы проверяете время на код, а также на то, как вы используете обычный код CUDA. Обычным решением является вставка вызова cudaDeviceSynchronize () перед закрытием области синхронизации . Это гарантирует, что все действия CUDA завершены, когда вы закончите измерение времени.

Когда я превратил то, что у вас есть, в законченный код с надлежащими методами синхронизации, код тяги был на самом деле быстрее. Ваш дизайн ядра неэффективен. Вот моя версия вашего кода, работающего на CUDA 10 на Tesla P100, показывающая, что время между двумя случаями примерно одинаково:

$ cat t469.cu
#include <thrust/transform.h>
#include <thrust/complex.h>
#include <thrust/device_ptr.h>
#include <thrust/execution_policy.h>
#include <cuComplex.h>

#include <iostream>
#include <time.h>
#include <sys/time.h>
#include <cstdlib>
#define USECPSEC 1000000ULL

long long dtime_usec(unsigned long long start){

  timeval tv;
  gettimeofday(&tv, 0);
  return ((tv.tv_sec*USECPSEC)+tv.tv_usec)-start;
}

typedef thrust::complex<float> comThr;

struct Complex_Mul_Complex :public thrust::binary_function<comThr, comThr, comThr>
{
    __host__ __device__
    comThr operator() (comThr a, comThr b) const{
        return a*b;
    }
};
double cpuSecond(){
  long long dt = dtime_usec(0);
  return dt/(double)USECPSEC;
}
__global__ void HardamarProductOnDeviceCC(cuComplex *Result, cuComplex *A, cuComplex *B, int N)
{
  unsigned int index = threadIdx.x + blockDim.x * blockIdx.x;

  if(index < N)
    Result[index] = cuCmulf(A[index],B[index]);
}

int main(){
int N = 720896;
cuComplex *d_Data1, *d_Data2;
cudaMalloc(&d_Data1, N*sizeof(d_Data1[0]));
cudaMalloc(&d_Data2, N*sizeof(d_Data2[0]));
// step 1: type convert (cuComplex->thrust)
comThr *thr_temp1 = reinterpret_cast<comThr*>(d_Data1);
thrust::device_ptr<comThr> thr_d_Data1 = thrust::device_pointer_cast(thr_temp1);

comThr *thr_temp2 = reinterpret_cast<comThr*>(d_Data2);
thrust::device_ptr<comThr> thr_d_Data2 = thrust::device_pointer_cast(thr_temp2);

// step 2: product and timing
Complex_Mul_Complex op_dot;
double iStart = cpuSecond();  // timing class
for(int i=0;i<1000;i++){    // loop 1000 times to get accurate time consumption
  thrust::transform(thrust::device,thr_d_Data1,thr_d_Data1+N,
    thr_d_Data2,thr_d_Data1,op_dot);
}
cudaDeviceSynchronize();
double iElapse = cpuSecond() - iStart;
std::cout << "thrust time consume: " << iElapse <<std::endl;

int blockSize;
int minGridSize;
int gridSize;
cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSize, HardamarProductOnDeviceCC, 0, 0);

gridSize = (N+blockSize-1)/blockSize;
std::cout << "gridsize: " << gridSize << " blocksize: " << blockSize << std::endl;
dim3 grid(gridSize);
dim3 block(blockSize);
iStart = cpuSecond();
for(int i=0;i<1000;i++){
    HardamarProductOnDeviceCC<<<grid,block>>>(d_Data1,d_Data1,d_Data2,N);
}
cudaDeviceSynchronize();
iElapse = cpuSecond() - iStart;
std::cout << "kernel time consume: " << iElapse <<std::endl;
}
$ nvcc -o t469 t469.cu
$ ./t469
thrust time consume: 0.033937
gridsize: 704 blocksize: 1024
kernel time consume: 0.0337021
$

Примечание: для того, чтобы я продемонстрировал правильность своего ответа, для меня важно предоставить полный код. Если вам нужна помощь от других, я предлагаю вам предоставить полный код, а не биты и кусочки, которые нужно собрать, а затем преобразовать в полный код, добавив включения и т. Д. Вы можете делать все, что пожелаете Конечно, но если вам будет проще помогать другим, вы можете обнаружить, что вам легче помочь.

...