GPU parallel verwenden mit CUDA
(EN google-translate)
(PL google-translate)
Die GPU des Jetson Nano besteht aus 128 Einheiten, die parallel rechnen können. Als Beispiel soll die Berechnung der Ausgänge einer Neuronenschicht damit umgesetzt werden. Hinweise zu diesem Thema finden sich hier:
Die N=128 Neuronen sind vollständig vernetzt und es gibt 128 Eingänge und Ausgänge. Das bedeutet, dass N*N=16384 Gewichtswerte w vorhanden sein müssen, die jeden Eingang mit jedem Ausgang verbinden.
Eine C-Funktion, die auf der CPU läuft und die geforderte Aufgabe erledigt sieht so aus:
void neuroSchicht(float* out, float* w, float* in) { for(unsigned int i=0;i<N;i++) { out[i]=0.0; for(unsigned int k=0;k<N;k++) { out[i] += in[k] * w[k+i*N]; } out[i] = 1.0 / (1.0 + exp(-d*out[i])); } }
Code 0-1: C-Funktion,
Dazu im Vergleich sieht die Funktion, die das gleiche macht, aber die Verarbeitung verteilt auf 128 Prozessoren ausführt so aus:
__global__ void neuroSchichtGPU(float* out, float* w, float* in) { int i = blockIdx.x; int k=0; if(i<N) { out[i]=0.0; for(k=0;k<N;k++) { out[i] += in[k] * w[k+i*N]; } out[i] = 1.0 / (1.0 + exp(-d*out[i])); } }
Code 0-2: C-Funktion für GPU.
|
|
#include <iostream> #include <cuda_runtime.h> #include <math.h> #include <time.h> #include <stdlib.h> #define N 127 #define d 1.0 /* CPU-Funktion repräsentiert Neuronenschicht mit N Neuronen also: N Eingängen in N Ausgängen out N*N Gewichten w */ void neuroSchicht(float* out, float* w, float* in) { for(unsigned int i=0;i<N;i++) { out[i]=0.0; for(unsigned int k=0;k<N;k++) { out[i] += in[k] * w[k+i*N]; } out[i] = 1.0 / (1.0 + exp(-d*out[i])); } } //##### gleiche Funktion parallelisiert für N Blöcke, bei der die Block-ID bestimmt, welcher Output berechnet wird __global__ void neuroSchichtGPU(float* out, float* w, float* in) { int i = blockIdx.x; int k=0; if(i<N) { out[i]=0.0; for(k=0;k<N;k++) { out[i] += in[k] * w[k+i*N]; } out[i] = 1.0 / (1.0 + exp(-d*out[i])); } } //##### Variante für nur EINE nicht parallelisierte GPU: __global__ void neuroSchichtEinBlock(float* out, float* w, float* in) { for(unsigned int i=0;i<N;i++) { out[i]=0.0; for(unsigned int k=0;k<N;k++) { out[i] += in[k] * w[k+i*N]; } out[i] = 1.0 / (1.0 + exp(-d*out[i])); } } int main() { //##### Declare the variables float *inCPU; float *outCPU; float *wCPU; float *in; float *out; float *w; //##### Speicher auf CPU allokieren: inCPU = (float*)calloc(N,sizeof(float)); outCPU = (float*)calloc(N,sizeof(float)); wCPU = (float*)calloc(N*N,sizeof(float)); cudaMallocManaged(&in, N*sizeof(float)); cudaMallocManaged(&out, N*sizeof(float)); cudaMallocManaged(&w, N*N*sizeof(float)); // Seeding the random number generator srand(1000); //##### Gewichte und Inputs zufällig initialisieren for (unsigned int i = 0; i < N; i++) { outCPU[i] = 0.0; inCPU[i] = ((float)rand()/(float)(RAND_MAX)); // ]0..1[ for(unsigned int k = 0;k<N;k++) { wCPU[k+i*N] = 0.01*((float)rand()/(float)(RAND_MAX)); // ]0..0.01[ } } //##### Berechnung 1000000 mal auf CPU ausführen clock_t startzeit = clock(); //for (unsigned int i = 0; i < 1000000; i++) neuroSchicht(outCPU,wCPU,inCPU); clock_t endzeit = clock(); double dt_ms = 1000.0*((double)(endzeit - startzeit)/ CLOCKS_PER_SEC); std::cout << "1 Aufruf auf CPU dauerten "<< dt_ms<<" Millisekunden." << std::endl; //##### Ergebnis auf Konsole ausgeben /* std::cout << "Outputwerte:" << std::endl; for (unsigned int i = 0; i < N; i++) { std::cout<<"out("<<i<<")="<<outCPU[i]<<std::endl; } */ //##### Ausführung auf GPU in N Blöcken //startzeit = clock(); //for (unsigned int i = 0; i < 1000000; i++) //{ //##### Variablen der CPU auf GPU-Arrays kopieren: cudaMemcpy(in,inCPU, N*sizeof(float),cudaMemcpyHostToDevice); cudaMemcpy(out,outCPU, N*sizeof(float),cudaMemcpyHostToDevice); cudaMemcpy(w,wCPU, N*N*sizeof(float),cudaMemcpyHostToDevice); startzeit = clock(); neuroSchichtEinBlock<<<1, 1>>>(out,w,in); // neuroSchichtGPU<<<N, 1>>>(out,w,in); //cudaDeviceSynchronize(); //Warten, bis alle Blöcke fertig sind endzeit = clock(); //##### Variablen der GPU auf CPU-Arrays zurück kopieren: cudaMemcpy(inCPU,in, N*sizeof(float),cudaMemcpyDeviceToHost); cudaMemcpy(outCPU,out, N*sizeof(float),cudaMemcpyDeviceToHost); cudaMemcpy(wCPU,w, N*N*sizeof(float),cudaMemcpyDeviceToHost); //} //endzeit = clock(); dt_ms = 1000.0*((double)(endzeit - startzeit)/ CLOCKS_PER_SEC); std::cout << "1 Aufruf auf EINER GPU dauerten "<< dt_ms<<" Millisekunden." << std::endl; //for (unsigned int i = 0; i < 1000000; i++) //{ //##### Variablen der CPU auf GPU-Arrays kopieren: cudaMemcpy(in,inCPU, N*sizeof(float),cudaMemcpyHostToDevice); cudaMemcpy(out,outCPU, N*sizeof(float),cudaMemcpyHostToDevice); cudaMemcpy(w,wCPU, N*N*sizeof(float),cudaMemcpyHostToDevice); startzeit = clock(); // neuroSchichtEinBlock<<<1, 1>>>(out,w,in); neuroSchichtGPU<<<N, 1>>>(out,w,in); //cudaDeviceSynchronize(); //Warten, bis alle Blöcke fertig sind endzeit = clock(); //##### Variablen der GPU auf CPU-Arrays zurück kopieren: cudaMemcpy(inCPU,in, N*sizeof(float),cudaMemcpyDeviceToHost); cudaMemcpy(outCPU,out, N*sizeof(float),cudaMemcpyDeviceToHost); cudaMemcpy(wCPU,w, N*N*sizeof(float),cudaMemcpyDeviceToHost); //} //endzeit = clock(); dt_ms = 1000.0*((double)(endzeit - startzeit)/ CLOCKS_PER_SEC); std::cout << "1 Aufruf auf 127 GPU dauerten "<< dt_ms<<" Millisekunden." << std::endl; //##### Ergebnis auf Konsole ausgeben /* std::cout << "Outputwerte:" << std::endl; for (unsigned int i = 0; i < N; i++) { std::cout<<"out("<<i<<")="<<outCPU[i]<<std::endl; } */ //##### Releasing the memory cudaFree(in); cudaFree(out); cudaFree(w); free(inCPU); free(outCPU); free(wCPU); std::cout << "Code Execution Completed" << std::endl; return 0; }
Code 0-3: neuro003.cu Gesamtprogramm zur Berechnung einer Neuronenschicht.
Wenn der Programmquelltext in einer Datei neuro003.cu steht, dann wird er folgendermaßen kompiliert und gestartet: nvcc neuro003.cu -o neuro003 ./neuro003
Code 0-4: Verwendung
|
Ergebnisse
Nun würde man erwarten, dass die Abarbeitung auf der GPU 100mal so schnell geht. Jedoch ist diese nur circa vier mal so schnell:
1 Aufruf auf CPU dauerten 0.284 Millisekunden. 1 Aufruf auf EINER GPU dauerten 0.065 Millisekunden. 1 Aufruf auf 127 GPU dauerten 0.07 Millisekunden.
Code 0-5: Ergenisse
Mögliche Ursachen (steht in Frage):
|
Weitere Schritte: Auch hierfür eine JNI basierte Library für Processing erstellen.