В предыдущем посте мы настроили OpenCL, а теперь мы попробуем провести тест производительности. За основу возьмем пример cached_matrix_multiplication, где перемножаются две матрицы и результат записывается в третью. Kernel-функция там имеет такой вид:
- /**
- * This kernel function efficiently multiplies two matrices a[M,K] and b[K,N]
- * by caching submatrices from those input matrices in the device local memory.
- */
- __kernel void multiplyMatricesWithCache(__global int* a,
- __global int* b,
- __global int* c,
- const int M,
- const int N,
- const int K){
- /**
- * Declare the size of each submatrix (it must be
- * the same work-group size declared in the host code).
- */
- const int SUB_SIZE = 16;
- /**
- * Get work-item identifiers.
- */
- int colIndex = get_local_id(0);
- int rowIndex = get_local_id(1);
- int globalColIndex = get_global_id(0);
- int globalRowIndex = get_global_id(1);
- int index = (globalRowIndex * N) + globalColIndex;
- /**
- * Create submatrices that will cache the matrices A and B in local memory.
- */
- __local int aSub[SUB_SIZE][SUB_SIZE];
- __local int bSub[SUB_SIZE][SUB_SIZE];
- /**
- * Initialize accumulator register.
- */
- int sum = 0;
- /**
- * Loop over all submatrices.
- */
- const int nSub = K / SUB_SIZE;
- for(int s = 0; s < nSub; s++){
- /**
- * Load submatrices into local memory.
- */
- const int sCol = SUB_SIZE * s + colIndex;
- const int sRow = SUB_SIZE * s + rowIndex;
- aSub[rowIndex][colIndex] = a[globalRowIndex * K + sCol];
- bSub[rowIndex][colIndex] = b[sRow * N + globalColIndex];
- /**
- * Synchronize all work-items in this work-group.
- */
- barrier(CLK_LOCAL_MEM_FENCE);
- /**
- * Perform the computation for a single submatrix.
- */
- for(int k = 0; k < SUB_SIZE; k++){
- sum += aSub[rowIndex][k] * bSub[k][colIndex];
- }
- /**
- * Synchronize all work-items in this work-group.
- */
- barrier(CLK_LOCAL_MEM_FENCE);
- }
- /**
- * Store the final result in the matrix C.
- */
- c[index] = sum;
- }
cl::CommandQueue queue(context, device); queue.enqueueNDRangeKernel(kernel, cl::NullRange, cl::NDRange(N, M), cl::NDRange(WG_SIZE[0], WG_SIZE[1])); queue.enqueueReadBuffer(cBuf, CL_TRUE, 0, M * N * sizeof(int), c);Функция enqueueNDRangeKernel() принимает вторым параметром смещение (в данном случае ноль, то есть вычисления с начала массива), третьим параметром размерность обрабатываемых данных (данном случае это двумерный массив), а четвертый параметр это размерность группы. В нашем случае это массив 16 на 16, то есть 256 тредов в каждой группе. Более подробно можно почитать здесь.
На стороне CPU произведение матриц выполнено предельно просто:
- void seqMultiplyMatrices(int* a, int* b, int* c,
- const int M,
- const int N,
- const int K) {
- for (int i = 0; i < M; i++) {
- for (int j = 0; j < N; j++) {
- int sum = 0;
- for (int k = 0; k < K; k++) {
- sum += a[i*K + k] * b[j + k * N];
- }
- c[i*N + j] = sum;
- }
- }
- }
- void parMultiplyMatrices_CPU(int* a, int* b, int* c,
- const int M,
- const int N,
- const int K)
- {
- const auto thrCnt = std::thread::hardware_concurrency();
- std::vector<std::thread> workers;
- const int rod = N % thrCnt;
- const unsigned int bColCntPerThread = (N - rod) / thrCnt;
- for (int thrId = 0; thrId < thrCnt; ++thrId)
- {
- workers.push_back(std::thread([thrId, thrCnt, bColCntPerThread, a, b, c, M, N, K]()
- {
- std::vector<int> bColBuf(K);
- auto bColbegIdx = thrId * bColCntPerThread;
- bool isLastThread = (thrId == thrCnt - 1);
- auto bColEndIdx = isLastThread ? N : bColbegIdx + bColCntPerThread;
- //each thread iterate some amount of columns in B matrix
- for (auto bColIdx = bColbegIdx; bColIdx < bColEndIdx; ++bColIdx)
- {
- //fill column buffer for B matr
- for (int i = 0; i < K; ++i)
- {
- bColBuf[i] = b[i * N + bColIdx];
- }
- //multiply all rows of A to current column of B
- for (int aRowIdx = 0; aRowIdx < M; ++aRowIdx)
- {
- int tmp = 0;
- for (int aColIdx = 0; aColIdx < K; ++aColIdx)
- {
- tmp += a[aRowIdx * M + aColIdx] * bColBuf[aColIdx];
- }
- c[aRowIdx * M + bColIdx] = tmp;
- }
- }
- }));
- }
- for (std::thread& t : workers)
- {
- t.join();
- }
- workers.clear();
- }
И тут у нас вместо 0.163 сек получается 0.367 сек, то есть распараллеливание давало прирост всего лишь в 2.3 раза! А основной "ускоритель" вычисления - это именно использования кеша для столбцов. Что еще раз говорит нам об важности алгоритмов и понимания работы подсистемы памяти. Сам по себе мощный многоядерный процессор ничего не ускорит.
Тестовая платформа: Ryzen 7 3700X, 16GB RAM, RTX 3060
Код этого пример можно найти здесь.
Комментариев нет:
Отправить комментарий