it-swarm.com.de

Wie ordne ich Arrays innerhalb eines Kernels dynamisch zu?

Ich muss einige Arrays innerhalb der Kernelfunktion dynamisch zuweisen. Wie kann ich das machen?

Mein Code ist ungefähr so:

__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.  
}

Aber das wird nicht funktionieren. Wenn dies innerhalb des Host-Codes wäre, könnte ich malloc verwenden. cudaMalloc benötigt einen Zeiger auf Host und einen Zeiger auf Gerät. Innerhalb der Kernel-Funktion habe ich keinen Host-Zeiger.

Also was soll ich tun?

Wenn es zu lange dauert (einige Sekunden), um alle Arrays zuzuweisen (ich benötige ungefähr 4 der Größe n und 5 der Größe nn), ist dies kein Problem. Da wird der Kernel wohl mindestens 20 Minuten laufen.

20
Granada

Die dynamische Speicherzuweisung wird nur bei der Rechenfunktion 2.x und neuer unterstützt. Sie können entweder das neue C++ - Schlüsselwort oder malloc im Kernel verwenden, sodass Ihr Beispiel folgendermaßen aussehen könnte:

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

Dadurch wird Speicher auf einem lokalen Speicherlaufzeitheap zugewiesen, der die Lebensdauer des Kontexts hat. Stellen Sie daher sicher, dass Sie den Speicher freigeben, nachdem der Kernel ausgeführt wurde, wenn Sie beabsichtigen, den Speicher nicht erneut zu verwenden. Sie sollten auch beachten, dass auf den Laufzeit-Heap-Speicher nicht direkt von den Host-APIs aus zugegriffen werden kann. Daher können Sie keinen Zeiger, der innerhalb eines Kernels zugewiesen ist, beispielsweise als Argument an cudaMemcpy übergeben.

28
talonmies

@talonmies hat Ihre Frage zur dynamischen Zuweisung von Speicher innerhalb eines Kernels beantwortet. Dies ist als ergänzende Antwort gedacht, die auf die Leistung von __device__ malloc() abzielt, und eine Alternative, die Sie in Betracht ziehen möchten.

Das dynamische Zuordnen von Speicher im Kernel kann verlockend sein, da GPU-Code dadurch mehr wie CPU-Code aussehen kann. Es kann jedoch die Leistung ernsthaft beeinträchtigen. Ich schrieb einen in sich geschlossenen Test und habe ihn unten hinzugefügt. Der Test startet etwa 2,6 Millionen Threads. Jeder Thread füllt 16 Ganzzahlen des globalen Speichers mit einigen vom Threadindex abgeleiteten Werten auf, summiert die Werte zusammen und gibt die Summe zurück.

Der Test implementiert zwei Ansätze. Der erste Ansatz verwendet __device__ malloc() und der zweite Ansatz verwendet Speicher, der zugewiesen wird, bevor der Kernel ausgeführt wird.

Auf meinem 2.0-Gerät läuft der Kernel bei Verwendung von __device__ malloc() in 1500ms und bei Verwendung von vorab zugewiesenem Speicher in 27ms. Mit anderen Worten, der Test dauert 56x länger , wenn der Speicher dynamisch innerhalb des Kernels zugewiesen wird. Die Zeit beinhaltet die äußere Schleife cudaMalloc()/cudaFree(), die nicht Teil des Kernels ist. Wenn derselbe Kernel mehrmals mit der gleichen Anzahl von Threads gestartet wird, wie dies häufig der Fall ist, werden die Kosten für cudaMalloc()/cudaFree() über alle Kernel-Starts amortisiert. Damit ist der Unterschied noch höher und liegt bei ca. 60x.

Spekuliert denke ich, dass der Performance-Hit teilweise durch implizite Serialisierung verursacht wird. Die GPU muss wahrscheinlich alle gleichzeitigen Aufrufe von __device__ malloc() serialisieren, um jedem Anrufer separate Speicherblöcke bereitzustellen.

Die Version, die __device__ malloc() nicht verwendet, weist den gesamten GPU-Speicher zu, bevor der Kernel ausgeführt wird. Ein Zeiger auf den Speicher wird an den Kernel übergeben. Jeder Thread berechnet einen Index in den zuvor zugewiesenen Speicher, anstatt eine __device__ malloc() zu verwenden.

Das potenzielle Problem bei der Zuweisung von Arbeitsspeicher im Vorfeld besteht darin, dass, wenn nur einige Threads Speicher zuordnen müssen, und es nicht bekannt ist, welche Threads dies sind, dass für alle Threads Speicherplatz zugewiesen werden muss. Wenn nicht genügend Speicher vorhanden ist, ist es möglicherweise effizienter, die Anzahl der Threads pro Kernelaufruf zu reduzieren, als __device__ malloc(). Bei anderen Problemumgehungen wird wahrscheinlich reimplementiert, was __device__ malloc() im Hintergrund ausgeführt wird, und es wird ein ähnlicher Leistungseinbruch angezeigt.

Testen Sie die Leistung von __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;
}

Ausgabe:

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

Wenn der Wert von n und nn vor dem Aufruf des Kernels bekannt war, warum dann nicht den Speicher auf der Host-Seite cudaMalloc und den Gerätespeicherzeiger an den Kernel übergeben?

2
Hong Zhou

Lief ein Experiment basierend auf den Konzepten in @ rogerdahls Post. Annahmen:

  • 4 MB Speicher in 64B-Blöcken.
  • 1 GPU-Block und 32 Warp-Threads in diesem Block
  • Laufen Sie auf einem P100

Die malloc + free-Anrufe lokal vor der GPU schienen viel schneller zu sein als die Aufrufe cudaMalloc + cudaFree. Die Ausgabe des Programms:

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

Ich lasse den Code für timer.h und timer.cpp aus, aber hier ist der Code für den Test selbst:

#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();
}

Wenn Sie Fehler finden, bitte lmk in den Kommentaren, und ich werde versuchen, sie zu beheben.

Und ich habe sie nochmal mit größerem alles ausgeführt:

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;

Und cudaMalloc war noch viel langsamer:

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

Vielleicht solltest du testen

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

stattdessen 

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