CUDA pre strojové učenie: praktické aplikácie
Teraz, keď sme prebrali základy, poďme preskúmať, ako možno CUDA aplikovať na bežné úlohy strojového učenia.
-
Násobenie matice
Násobenie matice je základnou operáciou v mnohých algoritmoch strojového učenia, najmä v neurónových sieťach. CUDA môže túto operáciu výrazne urýchliť. Tu je jednoduchá implementácia:
__global__ void matrixMulKernel(float *A, float *B, float *C, int N) { int row = blockIdx.y * blockDim.y + threadIdx.y; int col = blockIdx.x * blockDim.x + threadIdx.x; float sum = 0.0f; if (row < N && col < N) { for (int i = 0; i < N; i++) { sum += A(row * N + i) * B(i * N + col); } C(row * N + col) = sum; } } // Host function to set up and launch the kernel void matrixMul(float *A, float *B, float *C, int N) { dim3 threadsPerBlock(16, 16); dim3 numBlocks((N + threadsPerBlock.x - 1) / threadsPerBlock.x, (N + threadsPerBlock.y - 1) / threadsPerBlock.y); matrixMulKernelnumBlocks, threadsPerBlock(A, B, C, N); }
Táto implementácia rozdeľuje výstupnú maticu na bloky, pričom každé vlákno počíta jeden prvok výsledku. Aj keď je táto základná verzia už rýchlejšia ako implementácia CPU pre veľké matice, je tu priestor na optimalizáciu pomocou zdieľanej pamäte a iných techník.
-
Konvolučné operácie
Konvolučné neurónové siete (CNN) silne spoliehať na konvolučné operácie. CUDA môže tieto výpočty výrazne urýchliť. Tu je zjednodušené 2D konvolučné jadro:
__global__ void convolution2DKernel(float *input, float *kernel, float *output, int inputWidth, int inputHeight, int kernelWidth, int kernelHeight) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; if (x < inputWidth && y < inputHeight) { float sum = 0.0f; for (int ky = 0; ky < kernelHeight; ky++) { for (int kx = 0; kx < kernelWidth; kx++) { int inputX = x + kx - kernelWidth / 2; int inputY = y + ky - kernelHeight / 2; if (inputX >= 0 && inputX < inputWidth && inputY >= 0 && inputY < inputHeight) { sum += input(inputY * inputWidth + inputX) * kernel(ky * kernelWidth + kx); } } } output(y * inputWidth + x) = sum; } }
Toto jadro vykonáva 2D konvolúciu, pričom každé vlákno počíta s jedným výstupným pixelom. V praxi by sofistikovanejšie implementácie využívali zdieľanú pamäť na zníženie prístupu do globálnej pamäte a optimalizáciu pre rôzne veľkosti jadra.
-
Stochastický gradientový zostup (SGD)
SGD je základný optimalizačný algoritmus strojového učenia. CUDA dokáže paralelizovať výpočet gradientov naprieč viacerými dátovými bodmi. Tu je zjednodušený príklad lineárnej regresie:
__global__ void sgdKernel(float *X, float *y, float *weights, float learningRate, int n, int d) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) { float prediction = 0.0f; for (int j = 0; j < d; j++) { prediction += X(i * d + j) * weights(j); } float error = prediction - y(i); for (int j = 0; j < d; j++) { atomicAdd(&weights(j), -learningRate * error * X(i * d + j)); } } } void sgd(float *X, float *y, float *weights, float learningRate, int n, int d, int iterations) { int threadsPerBlock = 256; int numBlocks = (n + threadsPerBlock - 1) / threadsPerBlock; for (int iter = 0; iter < iterations; iter++) { sgdKernel<<<numBlocks, threadsPerBlock>>>(X, y, weights, learningRate, n, d); } }
Táto implementácia aktualizuje váhy paralelne pre každý údajový bod. The atomicAdd
funkcia sa používa na bezpečné spracovanie súbežných aktualizácií závaží.
Optimalizácia CUDA pre strojové učenie
Zatiaľ čo vyššie uvedené príklady demonštrujú základy používania CUDA pre úlohy strojového učenia, existuje niekoľko optimalizačných techník, ktoré môžu ďalej zvýšiť výkon:
-
Coalesced Memory Access
GPU dosahujú špičkový výkon, keď vlákna v deformácii pristupujú k súvislým pamäťovým miestam. Zabezpečte, aby vaše dátové štruktúry a prístupové vzory podporovali kombinovaný prístup k pamäti.
-
Využitie zdieľanej pamäte
Zdieľaná pamäť je oveľa rýchlejšia ako globálna pamäť. Použite ho na vyrovnávanie často používaných údajov v rámci bloku vlákien.
Tento diagram znázorňuje architektúru viacprocesorového systému so zdieľanou pamäťou. Každý procesor má vlastnú vyrovnávaciu pamäť, ktorá umožňuje rýchly prístup k často používaným údajom. Procesory komunikujú prostredníctvom zdieľanej zbernice, ktorá ich spája s väčším priestorom zdieľanej pamäte.
Napríklad pri maticovom násobení:
__global__ void matrixMulSharedKernel(float *A, float *B, float *C, int N) { __shared__ float sharedA(TILE_SIZE)(TILE_SIZE); __shared__ float sharedB(TILE_SIZE)(TILE_SIZE); int bx = blockIdx.x; int by = blockIdx.y; int tx = threadIdx.x; int ty = threadIdx.y; int row = by * TILE_SIZE + ty; int col = bx * TILE_SIZE + tx; float sum = 0.0f; for (int tile = 0; tile < (N + TILE_SIZE - 1) / TILE_SIZE; tile++) { if (row < N && tile * TILE_SIZE + tx < N) sharedA(ty)(tx) = A(row * N + tile * TILE_SIZE + tx); else sharedA(ty)(tx) = 0.0f; if (col < N && tile * TILE_SIZE + ty < N) sharedB(ty)(tx) = B((tile * TILE_SIZE + ty) * N + col); else sharedB(ty)(tx) = 0.0f; __syncthreads(); for (int k = 0; k < TILE_SIZE; k++) sum += sharedA(ty)(k) * sharedB(k)(tx); __syncthreads(); } if (row < N && col < N) C(row * N + col) = sum; }
Táto optimalizovaná verzia využíva zdieľanú pamäť na zníženie prístupu do globálnej pamäte, čím sa výrazne zvyšuje výkon pre veľké matice.
-
Asynchrónne operácie
CUDA podporuje asynchrónne operácie, čo vám umožňuje prekrývať výpočty s prenosom dát. To je užitočné najmä v kanáloch strojového učenia, kde môžete pripraviť ďalšiu dávku údajov, zatiaľ čo sa aktuálna dávka spracováva.
cudaStream_t stream1, stream2; cudaStreamCreate(&stream1); cudaStreamCreate(&stream2); // Asynchronous memory transfers and kernel launches cudaMemcpyAsync(d_data1, h_data1, size, cudaMemcpyHostToDevice, stream1); myKernel<<<grid, block, 0, stream1>>>(d_data1, ...); cudaMemcpyAsync(d_data2, h_data2, size, cudaMemcpyHostToDevice, stream2); myKernel<<<grid, block, 0, stream2>>>(d_data2, ...); cudaStreamSynchronize(stream1); cudaStreamSynchronize(stream2);
-
Tenzorové jadrá
Pre pracovné zaťaženie strojového učenia, Tensor Cores NVIDIA (dostupné v novších architektúrach GPU) môže poskytnúť výrazné zrýchlenie operácií násobenia matíc a konvolúcie. Knižnice ako cuDNN a cuBLAS automaticky využívajú jadrá Tensor, keď sú k dispozícii.
Výzvy a úvahy
Zatiaľ čo CUDA ponúka obrovské výhody pre strojové učenie, je dôležité si uvedomiť potenciálne výzvy:
- Správa pamäte: Pamäť GPU je v porovnaní so systémovou pamäťou obmedzená. Efektívna správa pamäte je kľúčová, najmä pri práci s veľkými súbormi údajov alebo modelmi.
- Réžia prenosu dát: Prenos údajov medzi CPU a GPU môže byť prekážkou. Minimalizujte prenosy a ak je to možné, používajte asynchrónne operácie.
- Presnosť: GPU tradične vynikajú vo výpočtoch s jednou presnosťou (FP32). Podpora pre dvojitú presnosť (FP64) sa síce zlepšila, no často je pomalšia. Mnoho úloh strojového učenia môže fungovať dobre s nižšou presnosťou (napr. FP16), ktorú moderné GPU zvládajú veľmi efektívne.
- Zložitosť kódu: Zápis efektívneho kódu CUDA môže byť zložitejší ako kód CPU. Využitie knižníc ako cuDNNcuBLAS a rámce ako TensorFlow alebo PyTorch môžu pomôcť abstrahovať časť tejto zložitosti.
Ako modely strojového učenia rastú vo veľkosti a zložitosti, jeden GPU už nemusí stačiť na zvládnutie pracovného zaťaženia. CUDA umožňuje škálovať vašu aplikáciu na viacerých GPU, buď v rámci jedného uzla, alebo cez klaster.
Štruktúra programovania CUDA
Na efektívne využitie CUDA je nevyhnutné porozumieť jej programovacej štruktúre, ktorá zahŕňa písanie jadier (funkcií, ktoré bežia na GPU) a správu pamäte medzi hostiteľom (CPU) a zariadením (GPU).
Pamäť hostiteľa a zariadenia
V CUDA je pamäť spravovaná oddelene pre hostiteľa a zariadenie. Nasledujú hlavné funkcie používané na správu pamäte:
- cudaMalloc: Prideľuje pamäť v zariadení.
- cudaMemcpy: Kopíruje údaje medzi hostiteľom a zariadením.
- cudaFree: Uvoľňuje pamäť v zariadení.
Príklad: Sčítanie dvoch polí
Pozrime sa na príklad, ktorý sčítava dve polia pomocou CUDA:
__global__ void sumArraysOnGPU(float *A, float *B, float *C, int N) { int idx = threadIdx.x + blockIdx.x * blockDim.x; if (idx < N) C(idx) = A(idx) + B(idx); } int main() { int N = 1024; size_t bytes = N * sizeof(float); float *h_A, *h_B, *h_C; h_A = (float*)malloc(bytes); h_B = (float*)malloc(bytes); h_C = (float*)malloc(bytes); float *d_A, *d_B, *d_C; cudaMalloc(&d_A, bytes); cudaMalloc(&d_B, bytes); cudaMalloc(&d_C, bytes); cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice); cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice); int blockSize = 256; int gridSize = (N + blockSize - 1) / blockSize; sumArraysOnGPU<<<gridSize, blockSize>>>(d_A, d_B, d_C, N); cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost); cudaFree(d_A); cudaFree(d_B); cudaFree(d_C); free(h_A); free(h_B); free(h_C); return 0; }
V tomto príklade je pamäť alokovaná na hostiteľovi aj na zariadení, údaje sa prenesú do zariadenia a spustí sa jadro na vykonanie výpočtu.
Záver
CUDA je výkonný nástroj pre inžinierov strojového učenia, ktorí chcú urýchliť svoje modely a zvládnuť väčšie súbory údajov. Pochopením pamäťového modelu CUDA, optimalizáciou prístupu k pamäti a využitím viacerých GPU môžete výrazne zvýšiť výkon svojich aplikácií strojového učenia.