web-dev-qa-db-ja.com

カーネル内で配列を動的に割り当てる方法は?

カーネル関数内にいくつかの配列を動的に割り当てる必要があります。どうすればよいですか?

私のコードはそのようなものです:

__global__ func(float *grid_d,int n, int nn){  
    int i,j;  
    float x[n],y[nn];  
    //Do some really cool and heavy computations here that takes hours.  
}

しかし、それは機能しません。これがホストコード内にある場合は、mallocを使用できます。 cudaMallocには、ホストとデバイス上のその他のポインターが必要です。カーネル関数の中には、ホストポインタがありません。

だから、私は何をすべきですか?

すべての配列を割り当てるのに時間がかかりすぎる(数秒)場合(サイズnの約4とサイズnnの5が必要)、これは問題にはなりません。カーネルはおそらく少なくとも20分間実行されるためです。

20
Granada

動的メモリ割り当ては、コンピューティング機能2.x以降のハードウェアでのみサポートされます。カーネルでC++ newキーワードまたはmallocを使用できるため、例は次のようになります。

__global__ func(float *grid_d,int n, int nn){  
    int i,j;  
    float *x = new float[n], *y = new float[nn];   
}

これにより、コンテキストの存続期間を持つローカルメモリランタイムヒープにメモリが割り当てられるため、メモリを再度使用しない場合は、カーネルの実行が終了した後にメモリを解放してください。また、ランタイムヒープメモリにはホストAPIから直接アクセスできないため、たとえば、カーネル内に割り当てられたポインタを引数としてcudaMemcpyに渡すことはできないことに注意してください。

28
talonmies

@talonmiesは、カーネル内でメモリを動的に割り当てる方法についての質問に答えました。これは、__device__ malloc()のパフォーマンスと、検討したい代替案に対処するための補足的な回答として意図されています。

カーネルで動的にメモリを割り当てると、GPUコードがCPUコードのように見えるため、魅力的な場合があります。ただし、パフォーマンスに深刻な影響を与える可能性があります。私は自己完結型のテストを作成し、以下に含めました。このテストでは、約260万のスレッドが起動します。各スレッドは、スレッドインデックスから派生したいくつかの値を16の整数のグローバルメモリに入力し、値を合計して、合計を返します。

テストは2つのアプローチを実装します。最初のアプローチは__device__ malloc()を使用し、2番目のアプローチはカーネルが実行される前に割り当てられたメモリを使用します。

私の2.0デバイスでは、カーネルは__device__ malloc()を使用する場合は1500ミリ秒で実行され、事前に割り当てられたメモリを使用する場合は27ミリ秒で実行されます。言い換えると、メモリがカーネル内で動的に割り当てられている場合、テストの実行に56倍かかります。時間には、カーネルの一部ではない外部ループcudaMalloc()/cudaFree()が含まれます。よくあることですが、同じカーネルが同じスレッド数で何度も起動される場合、cudaMalloc()/cudaFree()のコストは、すべてのカーネル起動で償却されます。これにより、差はさらに大きくなり、約60倍になります。

推測では、パフォーマンスヒットの一部は暗黙的なシリアル化が原因だと思います。 GPUは、各呼び出し元に個別のメモリチャンクを提供するために、__device__ malloc()へのすべての同時呼び出しをおそらくシリアル化する必要があります。

__device__ malloc()を使用しないバージョンは、カーネルを実行する前にすべてのGPUメモリを割り当てます。メモリへのポインタがカーネルに渡されます。各スレッドは、__device__ malloc()を使用する代わりに、以前に割り当てられたメモリへのインデックスを計算します。

事前にメモリを割り当てる場合の潜在的な問題は、一部のスレッドのみがメモリを割り当てる必要があり、どのスレッドかがわからない場合、すべてのスレッドにメモリを割り当てる必要があることです。そのための十分なメモリがない場合は、__device__ malloc()を使用して、カーネル呼び出しごとのスレッド数を減らす方が効率的です。他の回避策は、おそらく__device__ malloc()がバックグラウンドで実行していることを再実装することになり、同様のパフォーマンスヒットが発生します。

__device__ malloc()のパフォーマンスをテストします。

#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>

const int N_ITEMS(16);

#define USE_DYNAMIC_MALLOC

__global__ void test_malloc(int* totals)
{
  int tx(blockIdx.x * blockDim.x + threadIdx.x);

  int* s(new int[N_ITEMS]);

  for (int i(0); i < N_ITEMS; ++i) {
    s[i] = tx * i;
  }

  int total(0);
  for (int i(0); i < N_ITEMS; ++i) {
    total += s[i];
  }

  totals[tx] = total;

  delete[] s;
}

__global__ void test_malloc_2(int* items, int* totals)
{
  int tx(blockIdx.x * blockDim.x + threadIdx.x);

  int* s(items + tx * N_ITEMS);

  for (int i(0); i < N_ITEMS; ++i) {
    s[i] = tx * i;
  }

  int total(0);
  for (int i(0); i < N_ITEMS; ++i) {
    total += s[i];
  }

  totals[tx] = total;
}

int main()
{
  cudaError_t cuda_status;

  cudaSetDevice(0);

  int blocks_per_launch(1024 * 10);
  int threads_per_block(256);

  int threads_per_launch(blocks_per_launch * threads_per_block);

  int* totals_d;
  cudaMalloc((void**)&totals_d, threads_per_launch * sizeof(int));

  cudaEvent_t start, stop;
  cudaEventCreate(&start);
  cudaEventCreate(&stop);

  cudaDeviceSynchronize();
  cudaEventRecord(start, 0);

#ifdef USE_DYNAMIC_MALLOC
  cudaDeviceSetLimit(cudaLimitMallocHeapSize, threads_per_launch * N_ITEMS * sizeof(int));

  test_malloc<<<blocks_per_launch, threads_per_block>>>(totals_d);
#else
  int* items_d;
  cudaMalloc((void**)&items_d, threads_per_launch * sizeof(int) * N_ITEMS);

  test_malloc_2<<<blocks_per_launch, threads_per_block>>>(items_d, totals_d);

  cudaFree(items_d);
#endif

  cuda_status = cudaDeviceSynchronize();
  if (cuda_status != cudaSuccess) {
    printf("Error: %d\n", cuda_status);
    exit(1);
  }

  cudaEventRecord(stop, 0);
  cudaEventSynchronize(stop);
  float elapsedTime;
  cudaEventElapsedTime(&elapsedTime, start, stop);

  printf("Elapsed: %f\n", elapsedTime);

  int* totals_h(new int[threads_per_launch]);
  cuda_status = cudaMemcpy(totals_h, totals_d, threads_per_launch * sizeof(int), cudaMemcpyDeviceToHost);
  if (cuda_status != cudaSuccess) {
    printf("Error: %d\n", cuda_status);
    exit(1);
  }

  for (int i(0); i < 10; ++i) {
    printf("%d ", totals_h[i]);
  }
  printf("\n");

  cudaFree(totals_d);
  delete[] totals_h;

  return cuda_status;
}

出力:

C:\rd\projects\test_cuda_malloc\Release>test_cuda_malloc.exe
Elapsed: 27.311169
0 120 240 360 480 600 720 840 960 1080

C:\rd\projects\test_cuda_malloc\Release>test_cuda_malloc.exe
Elapsed: 1516.711914
0 120 240 360 480 600 720 840 960 1080
13
Roger Dahl

カーネルが呼び出される前にnとnnの値がわかっている場合は、ホスト側でメモリをcudaMallocして、カーネルへのデバイスメモリポインターを渡さないのはなぜですか。

2
Hong Zhou

@rogerdahlの投稿の概念に基づいて実験を実行しました。仮定:

  • 64Bチャンクで割り当てられた4MBのメモリ。
  • 1つのGPUブロックとそのブロック内の32のワープスレッド
  • P100で実行

GPUに対してローカルなmalloc + free呼び出しは、cudaMalloc + cudaFree呼び出しよりもはるかに高速であるように見えました。プログラムの出力:

Starting timer for cuda malloc timer
Stopping timer for cuda malloc timer
         timer for cuda malloc timer took 1.169631s
Starting timer for device malloc timer
Stopping timer for device malloc timer
         timer for device malloc timer took 0.029794s

timer.htimer.cppのコードは省略していますが、テスト自体のコードは次のとおりです。

#include "cuda_runtime.h"
#include <stdio.h>
#include <thrust/system/cuda/error.h>

#include "timer.h"

static void CheckCudaErrorAux (const char *, unsigned, const char *, cudaError_t);
#define CUDA_CHECK_RETURN(value) CheckCudaErrorAux(__FILE__,__LINE__, #value, value)

const int BLOCK_COUNT = 1;
const int THREADS_PER_BLOCK = 32;
const int ITERATIONS = 1 << 12;
const int ITERATIONS_PER_BLOCKTHREAD = ITERATIONS / (BLOCK_COUNT * THREADS_PER_BLOCK);

const int ARRAY_SIZE = 64;


void CheckCudaErrorAux (const char *file, unsigned line, const char *statement, cudaError_t err) {
    if (err == cudaSuccess)
        return;
    std::cerr << statement<<" returned " << cudaGetErrorString(err) << "("<<err<< ") at "<<file<<":"<<line << std::endl;
    exit (1);
}

__global__ void mallocai() {
    for (int i = 0; i < ITERATIONS_PER_BLOCKTHREAD; ++i) {
        int * foo;
        foo = (int *) malloc(sizeof(int) * ARRAY_SIZE);
        free(foo);
    }
}

int main() {

    Timer cuda_malloc_timer("cuda malloc timer");

    for (int i = 0; i < ITERATIONS; ++ i) {
        if (i == 1) cuda_malloc_timer.start(); // let it warm up one cycle
        int * foo;
        cudaMalloc(&foo, sizeof(int) * ARRAY_SIZE);
        cudaFree(foo);
    }
    cuda_malloc_timer.stop_and_report();
    CUDA_CHECK_RETURN(cudaDeviceSynchronize());

    Timer device_malloc_timer("device malloc timer");
    device_malloc_timer.start();
    mallocai<<<BLOCK_COUNT, THREADS_PER_BLOCK>>>();
    CUDA_CHECK_RETURN(cudaDeviceSynchronize());
    device_malloc_timer.stop_and_report();
}

間違いを見つけた場合は、コメントでlmkを送信してください。修正を試みます。

そして、私はそれらをより大きなもので再び実行しました:

const int BLOCK_COUNT = 56;
const int THREADS_PER_BLOCK = 1024;
const int ITERATIONS = 1 << 18;
const int ITERATIONS_PER_BLOCKTHREAD = ITERATIONS / (BLOCK_COUNT * THREADS_PER_BLOCK);

const int ARRAY_SIZE = 1024;

そしてcudaMallocはまだかなり遅くなりました:

Starting timer for cuda malloc timer
Stopping timer for cuda malloc timer
         timer for cuda malloc timer took 74.878016s
Starting timer for device malloc timer
Stopping timer for device malloc timer
         timer for device malloc timer took 0.167331s
0
ragerdl

たぶん、あなたはテストする必要があります

cudaMalloc(&foo,sizeof(int) * ARRAY_SIZE * ITERATIONS);
cudaFree(foo);

代わりに

for (int i = 0; i < ITERATIONS; ++ i) {
    if (i == 1) cuda_malloc_timer.start(); // let it warm up one cycle
    int * foo;
    cudaMalloc(&foo, sizeof(int) * ARRAY_SIZE);
    cudaFree(foo);
}
0
Tyrandro