2014-09-17 32 views
5

Poniżej znajduje się jądro opencl, które wykonuje blokowane mnożenie macierzy dla wielu niezależnych macierzy. selectMatrixA i selectMatrixB przechowują wiele macierzy (ten sam rozmiar i kwadratowe macierze) w głównym rzędzie.Optymalizowanie kodowania mnożenia macierzy batched macierzy

// Matrix multiplication: C = A * B. 


#define BLOCK_SIZE 20 
#define MATRIX_SIZE 100 * 100 

#define BLOCK_DIMX 5 // Number of blocks in the x dimension 

__kernel void 
batchedMatrixMul(__global float *selectMatrixC, __global float *selectMatrixA, __global 
float *selectMatrixB, int wA, int wB) 
{ 
    // Block index 
    int bx = get_group_id(0); 
    int by = get_group_id(1); 


    __global float *C = selectMatrixC + (bx/BLOCK_DIMX) * MATRIX_SIZE; 
    __global float *A = selectMatrixA + (bx/BLOCK_DIMX) * MATRIX_SIZE; 
    __global float *B = selectMatrixB + (bx/BLOCK_DIMX) * MATRIX_SIZE; 



    int tx = get_local_id(0); 
    int ty = get_local_id(1); 

    float Csub = 0; 

    // Identify the row and column of the C matrix to work on 

    int Row = (by * BLOCK_SIZE) + ty; 
    int Col = ((bx %(BLOCK_DIMX)) * BLOCK_SIZE) + tx; 

    // Declaration of the local memory array As used to store the sub-matrix of A 
    __local float As[BLOCK_SIZE][BLOCK_SIZE]; 

    // Declaration of the local memory array Bs used to store the sub-matrix of B 
    __local float Bs[BLOCK_SIZE][BLOCK_SIZE]; 

    // Loop over all the sub-matrices of A and B required to compute the block sub-matrix 
    for (int m = 0; m < wA/BLOCK_SIZE; ++m) 
    { 

     // Load the matrices from global memory to local memory. Each thread loads one 
     //element of each matrix 
     As[ty][tx] = A[Row * wA + m * BLOCK_SIZE + tx]; 
     Bs[ty][tx] = B[(m * BLOCK_SIZE + ty)*wA + Col]; 

     // Synchronize to make sure the matrices are loaded 
     barrier(CLK_LOCAL_MEM_FENCE); 

     // Multiply the two matrices together each thread computes one element of the block 
     //sub-matrix 
     for (int k = 0; k < BLOCK_SIZE; ++k) 
      Csub += As[ty][k] * Bs[k][tx]; 

     // Synchronize to make sure that the preceding computation is done before loading 
     //two new sub-matrices of A and B in the next iteration 
     barrier(CLK_LOCAL_MEM_FENCE); 

    } 

    // Write the block sub-matrix to device memory each thread writes one element 
    C[Row * wA + Col] = Csub; 

} 

Oto jak uruchomić jądra:

localWorkSize[0] = BLOCK_SIZE; 
localWorkSize[1] = BLOCK_SIZE; 

// for a 100 X 100 matrix, MATRIX_DIMX = MATRIX_DIMY = 100 
globalWorkSize[0] = MATRIX_DIMX * NUM_MATRICES; 
globalWorkSize[1] = MATRIX_DIMY ; 

cl_event    event; 
errcode = clEnqueueNDRangeKernel(clCommandQueue, 
      clKernel, 2, NULL, globalWorkSize, 
      localWorkSize, 0, NULL, &event); 

Poniżej znajdują się niektóre numery osiągi podczas jazdy to na NVIDIA Siatka K520:

1. matrix size:100 X 100 . Number of matrices = 20000. Time taken for multiplication = 
0.262 seconds. As shown in the code, the block size was set to 20. Block size of 10 was 
slower. This calculates to around 152 GFLOPS 

2. matrix size: 10000 X 10000. Number of matrices = 1. Time taken for multiplication = 10.6 
seconds. Here also the block size was 20. Using a block size of 50 is not possible due to 
the size of the local memory. 

Czy ktoś mógłby mi pomóc zrozumieć dlaczego kod działa wolno i dlaczego 2. jest znacznie wolniejszy niż 1. Jestem nowicjuszem w OpenCL i chcę nauczyć się optymalizować kod w oparciu o podstawowe detale architektoniczne.

+0

próbowałeś proste podejście iloczynu skalarnego, aby rozwiązać ten problem i uzyskać bazowy pomiar wydajności? może każdy element pracy może obliczyć pojedynczy element w C, wykonując operację 100 punktową. – mfa

+0

czy rozmiary matrycy muszą wynosić 100x100 i 10k x 10k? – mfa

+0

jest to wymaganie, aby rozwiązać kompletną operację mnożenia za pomocą jednej grupy roboczej, czy może to zrobić z wieloma grupami? – mfa

Odpowiedz

1

Moim zdaniem powód, dla którego 2. jest o wiele wolniejszy, jest taki, że wzorzec dostępu do multiplikacji macierzy nie jest tak przyjazny dla pamięci podręcznej. Jeśli chcesz uzyskać pierwszą wartość pierwszego wiersza i pierwszej wartości drugiego rzędu, są one przechowywane w pamięci bardzo daleko od siebie. Jeśli wzrasta rozmiar matrycy, są one przechowywane jeszcze dalej od siebie. Doprowadzi to do wielu błędów w pamięci podręcznej.

Nie mam żadnego osobistego doświadczenia w rozmnażaniu macierzy, ale pomyślałem, że może być możliwe zapisanie twoich danych w Z-order curve, aby uzyskać więcej wzoru przyjaznego pamięci podręcznej. Z odniesień Wikipedii wydaje się, że coś takiego zostało zrobione przez Valsalam & al 2002.

Inną szybką naprawą, którą chciałbym wypróbować przed użyciem dużej ilości czasu na zamawianie Z, jest użycie zmiennych prywatnych i pozbycie się barier. Nawet jeśli wymaga większej ilości ładunków z pamięci globalnej, możliwe, że kompilator może znacznie lepiej zoptymalizować ten kod.

3

Powodem, dla którego pierwszy test jest o wiele szybszy, jest różnica w ilości pracy wykonywanej przez każdy test. Właściwie współczynnik 50x.

Big-O dla mnożenia macierzy kwadratowej to O (n^3). Zobacz: why is the time complexity of square matrix multiplication defined as O(n^3)? W rezultacie macierz kwadratu 10k wymaga 1 miliona razy więcej pracy niż mnożenie 100x100. Twoje 20000 wykonań mnożenia 100x100 nie rekompensuje ogromnej ilości pracy potrzebnej do jednokrotnego pomnożenia dużych macierzy.

Mnożenie macierzy to tylko wiele produktów dot. Twój algorytm dzieli produkty z kropek na grupy, aby ułatwić obsługę i nie używa żadnych specjalnych sztuczek, aby zmniejszyć liczby w poniższych obliczeniach.

dla małych testu matrycy:

Total dot products: 10^4 
MADs per dot product: 10^2 
Total matrix-multiply operations: 20000 = 2*10^4 
Total multiply-adds: 2* 10^(4+2+4) = 2*10^10 = 20,000,000,000 

20 miliardów dolarów.

Duża testu matrix

Total dot products: 10^8 
MADs per dot product: 10^4 
Total multiply operations: 1 (or 10^0) 
Grand total multiply-adds: 10^(8 + 4 + 0) = 10^12 = 1,000,000,000,000 

1000 miliard.

Twój test 10000x10000 był technicznie szybszy - chrupanie 50x więcej operacji w zaledwie 40x więcej czasu pracy.

Więcej informacji na temat "specjalnych trików" tutaj: http://en.wikipedia.org/wiki/Strassen_algorithm.Chociaż ten algorytm nie jest uważany za praktyczny w obliczeniach GPU. Istnieją również skomplikowane algorytmy, ale najczęściej używa się metody brute-force na sprzęcie graficznym.

Dlaczego jądro działa ogólnie? Istnieje wiele różnych optymalizacji, których możesz użyć, aby przyspieszyć działanie. Poniżej znajduje się tylko kilka z nich, możesz google i eksperymentować ze sobą. Pewnie natkniesz się na niektóre, o których tu nie wspomniałem.

  • Optymalizuj grupę roboczą i rozmiary bloków. zobacz opencl PREFERRED_WORK_GROUP_SIZE
  • Użyj typu danych float4. opencl zawiera funkcję produktu dot, która oblicza produkt kropki dla typów danych floatn.
  • Transponuj macierz B przed uruchomieniem jądra. możesz użyć innego jądra do transpozycji.