▶ 矩阵乘法,按照书里的内容进行了几方面的优化,包括局部内存,矢量数据类型,寄存器,流水线等。
● 最直接的乘法。调用时 main.c 中使用 size_t globalSize[2] = { rowA, colB }, localSize[2] = { 16, 16 }; 。rowA 蕴含在 get_global_id(0) 中了,不再出现在函数中,后面的几种方法也如此。
1 // multiply.cl 2 __kernel void multiply01(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB) 3 { 4 const int row = get_global_id(0), col = get_global_id(1); 5 int k; 6 float sum; 7 for (k = 0, sum = 0.0f; k < colA; k++) 8 sum += inputA[row * colA + k] * inputB[k * colB + col]; 9 outputC[row * colB + col] = sum; 10 return; 11 }
1 // main.c 2 #include <stdio.h> 3 #include <math.h> 4 #include <stdlib.h> 5 #include <time.h> 6 #include <cl.h> 7 8 const int rowA = 4096, colA = 1024, colB = 2048; 9 //const int rowA = 128, colA = 128, colB = 128; // 测试用,刚够 multiply05 的 1 组 10 const char *sourceText = "D:\\Code\\OpenCL\\multiply.cl"; 11 12 bool floatEq(const float a, const float b)// 相等返回 1 13 { 14 if (b == 0) 15 return fabs(a) < 0.001; 16 return fabs(a / b - 1) < 0.001; 17 } 18 19 int readText(const char* kernelPath, char **pcode)// 读取文本文件放入 pcode,返回字符串长度 20 { 21 FILE *fp; 22 int size; 23 //printf("<readText> File: %s\n", kernelPath); 24 fopen_s(&fp, kernelPath, "rb"); 25 if (!fp) 26 { 27 printf("Open kernel file failed\n"); 28 getchar(); 29 exit(-1); 30 } 31 if (fseek(fp, 0, SEEK_END) != 0) 32 { 33 printf("Seek end of file failed\n"); 34 getchar(); 35 exit(-1); 36 } 37 if ((size = ftell(fp)) < 0) 38 { 39 printf("Get file position failed\n"); 40 getchar(); 41 exit(-1); 42 } 43 rewind(fp); 44 if ((*pcode = (char *)malloc(size + 1)) == NULL) 45 { 46 printf("Allocate space failed\n"); 47 getchar(); 48 exit(-1); 49 } 50 fread(*pcode, 1, size, fp); 51 (*pcode)[size] = '\0'; 52 fclose(fp); 53 return size + 1; 54 } 55 56 int main() 57 { 58 int i, j, k, correct; 59 float *A, *B, *C, tempSum; 60 //char info[1024] = { 0 }; 61 clock_t time; 62 63 A = (float*)malloc(sizeof(float) * rowA * colA); 64 B = (float*)malloc(sizeof(float) * colA * colB); 65 C = (float*)malloc(sizeof(float) * rowA * colB); 66 srand(2); 67 for (i = 0; i < rowA * colA; A[i] = rand() & 0xF, i++); 68 for (i = 0; i < colA * colB; B[i] = rand() & 0xF, i++); 69 for (i = 0; i < rowA * colB; C[i] = 0, i++); 70 71 // 初始化平台到创建命令队列 72 cl_int status; 73 cl_uint nPlatform; 74 clGetPlatformIDs(0, NULL, &nPlatform); 75 cl_platform_id *listPlatform = (cl_platform_id*)malloc(nPlatform * sizeof(cl_platform_id)); 76 clGetPlatformIDs(nPlatform, listPlatform, NULL); 77 cl_uint nDevice; 78 clGetDeviceIDs(listPlatform[0], CL_DEVICE_TYPE_ALL, 0, NULL, &nDevice); 79 cl_device_id *listDevice = (cl_device_id*)malloc(nDevice * sizeof(cl_device_id)); 80 clGetDeviceIDs(listPlatform[0], CL_DEVICE_TYPE_ALL, nDevice, listDevice, NULL); 81 cl_context context = clCreateContext(NULL, nDevice, listDevice, NULL, NULL, &status); 82 cl_command_queue queue = clCreateCommandQueue(context, listDevice[0], NULL, &status); 83 84 // 缓冲区 85 cl_mem bufferA = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(float) * rowA * colA, A, &status); 86 cl_mem bufferB = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(float) * colA * colB, B, &status); 87 cl_mem bufferC = clCreateBuffer(context, CL_MEM_WRITE_ONLY, sizeof(float) * rowA * colB, NULL, &status); 88 89 // 程序与内核参数 90 char *code; 91 size_t codeLength = readText(sourceText, &code); 92 cl_program program = clCreateProgramWithSource(context, 1, (const char**)&code, &codeLength, &status); 93 status = clBuildProgram(program, nDevice, listDevice, NULL, NULL, NULL); 94 //clGetProgramBuildInfo(program, listDevice[0], CL_PROGRAM_BUILD_LOG, 1024, info, NULL); 95 //printf("\n%s\n", info); 96 cl_kernel kernel = clCreateKernel(program, "multiply01", &status); 97 clSetKernelArg(kernel, 0, sizeof(cl_mem), &bufferA); 98 clSetKernelArg(kernel, 1, sizeof(cl_mem), &bufferB); 99 clSetKernelArg(kernel, 2, sizeof(cl_mem), &bufferC); 100 clSetKernelArg(kernel, 3, sizeof(int), &colA); 101 clSetKernelArg(kernel, 4, sizeof(int), &colB); 102 size_t globalSize[2] = { rowA, colB }, localSize[2] = { 16, 16 };// 注意不同函数需要调整工作组网格参数 103 104 // 执行内核 105 time = clock(); 106 status = clEnqueueNDRangeKernel(queue, kernel, 2, NULL, globalSize, localSize, 0, NULL, NULL); 107 clFinish(queue); 108 printf("\nTime kernel : %d ms\n", clock() - time); 109 110 // 返回并检查结果 111 clEnqueueReadBuffer(queue, bufferC, CL_TRUE, 0, sizeof(float) * rowA * colB, C, 0, NULL, NULL); 112 for (i = 0, correct = 1; i < rowA && correct; i++) 113 { 114 for (j = 0; j < colB && correct; j++) 115 { 116 for (k = 0, tempSum = 0.0f; k < colA; tempSum += A[i * colA + k] * B[k * colB + j], k++); 117 if (!floatEq(tempSum, C[i * colB + j])) 118 { 119 printf("Error at [%d, %d], calculation: %f, reference: %f\n", i, j, C[i*colA + j], tempSum); 120 correct = 0; 121 } 122 } 123 } 124 if (correct) 125 printf("Result correct.\n"); 126 printf("Time total: %d ms\n", clock() - time); 127 128 // 释放资源 129 free(A); 130 free(B); 131 free(C); 132 free(listPlatform); 133 free(listDevice); 134 clReleaseContext(context); 135 clReleaseCommandQueue(queue); 136 clReleaseProgram(program); 137 clReleaseKernel(kernel); 138 clReleaseMemObject(bufferA); 139 clReleaseMemObject(bufferB); 140 clReleaseMemObject(bufferC); 141 getchar(); 142 return 0; 143 }
● 输出结果。纯 CPU 串行计算的版本花了 145730 ms,开 O3 后优化为 9157 ms(真是 9 秒)
Time kernel : 228 ms Result correct. Time total: 112593 ms// 时间全花在检查结果上了
● 使用局部内存优化,一个工作组负责输出 outputC 中大小为 TILE_DIM * TILE_DIM 的矩阵。每次从 inputA 和 inputB 中分别取出一个该大小的子矩阵放入局部内存中(Asub 和 Bsub),完成该部分的乘法运算,再抓取下一对子矩阵(inputA 中向右挪动,inputB 中向下挪)。调用时在 mian.c 中修改核函数名字就行
1 // multiply.cl 2 #define TILE_DIM 16 3 4 __kernel void multiply02(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB) 5 { 6 __local float Asub[TILE_DIM][TILE_DIM], Bsub[TILE_DIM][TILE_DIM]; 7 const int localRow = get_local_id(0), localCol = get_local_id(1); 8 const int groupRow = get_group_id(0) * get_local_size(0), groupCol = get_group_id(1) * get_local_size(1);// 当前工作项的首元素位置 9 const int nTile = colA / TILE_DIM; // 行程长除以块长,即需要计算的方块数 10 int t, k; 11 float acc; 12 for (t = 0, acc = 0.0f; t < nTile; t++) 13 { 14 Asub[localRow][localCol] = inputA[(groupRow + localRow) * colA + TILE_DIM * t + localCol]; // 读取块A,注意列要用 t 来算 15 Bsub[localCol][localRow] = inputB[(TILE_DIM * t + localRow) * colB + groupCol + localCol]; // 读取块B,注意行要用 t 来算,注意交换了 Bsub 的行列,优化访问 16 //Bsub[localRow][localCol] = inputB[(t * TILE_DIM + localRow) * colB + groupCol + localCol];// 不交换 Bsub 行列的情形,与上一行相互替换 17 barrier(CLK_LOCAL_MEM_FENCE); 18 19 for (k = 0; k < TILE_DIM; k++) // 在局部内存上计算累进,acc 为各工作项私有,不会有冲突 20 acc += Asub[localRow][k] * Bsub[localCol][k]; 21 //acc += Asub[localRow][k] * Bsub[k][localCol]; // 不交换 Bsub 行列的情形,与上一行相互替换 22 barrier(CLK_LOCAL_MEM_FENCE); // 保证整个方块算完,才能进下一个方块 23 } 24 outputC[(groupRow + localRow) * colB + groupCol + localCol] = acc; 25 }
● 输出结果,有明显提高。不交换 Bsub 行列的情形时间稍长,约 91 ms。
Time kernel : 81 ms Result correct. Time total: 111484 ms
● 在 multiply02 的基础上,将 Asub 扩展为 float4 类型,四个元素分布在同一列隔 TILE_DIM 的位置上,如 Asub[0][0] == (float4)(A[0][0], A[16][0], A[32][0], A[48][0]),Bsub保持不变(为了方便后面 multiply04 的理解,这里先变A不变B),计算乘法时,4个 Asub 中的元素同时与 Bsub 相乘,最后输出到 outputC 时再分开,相当于一个工作项负责计算 outputC 中同一列中 4 个分散位置上的元素。调用时 main.c 中使用 size_t globalSize[2] = { rowA / 4, colB }, localSize[2] = { 16, 16 }; 。
1 // multiply.cl 2 #define TILE_DIM 16 3 4 __kernel void multiply03(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB) 5 { 6 __local float4 Asub[TILE_DIM][TILE_DIM]; // Asub 扩展为 float4,相当于原来的 4 倍大小 7 __local float Bsub[TILE_DIM][TILE_DIM]; // Bsub 保持不变 8 const int localRow = get_local_id(0), localCol = get_local_id(1); 9 const int groupRow = get_group_id(0) * get_local_size(0) * 4, globalCol = get_global_id(1); // 注意不同的工作组在行方向上相差为 get_local_size(0) * 4 10 const int nTile = colA / TILE_DIM; 11 int t, k; 12 float4 acc = (float4)(0.f), temp; 13 for (t = 0; t < nTile; t++) 14 { 15 Asub[localRow][localCol] = (float4)(inputA[(groupRow + TILE_DIM * 0 + localRow) * colA + TILE_DIM * t + localCol], // 从 A 中离散的 4 个位置上取元素放入 Asub 16 inputA[(groupRow + TILE_DIM * 1 + localRow) * colA + TILE_DIM * t + localCol], 17 inputA[(groupRow + TILE_DIM * 2 + localRow) * colA + TILE_DIM * t + localCol], 18 inputA[(groupRow + TILE_DIM * 3 + localRow) * colA + TILE_DIM * t + localCol]); 19 Bsub[localCol][localRow] = inputB[(TILE_DIM * t + localRow) * colB + globalCol]; 20 barrier(CLK_LOCAL_MEM_FENCE); 21 22 for (k = 0; k < TILE_DIM; k++) // 在局部内存上计算累进,每次计算四个位置乘法 23 acc += Asub[localRow][k] * Bsub[localCol][k]; 24 barrier(CLK_LOCAL_MEM_FENCE); 25 } 26 outputC[(groupRow + TILE_DIM * 0 + localRow) * colB + globalCol] = acc.x; // 输出时分散到各位置 27 outputC[(groupRow + TILE_DIM * 1 + localRow) * colB + globalCol] = acc.y; 28 outputC[(groupRow + TILE_DIM * 2 + localRow) * colB + globalCol] = acc.z; 29 outputC[(groupRow + TILE_DIM * 3 + localRow) * colB + globalCol] = acc.w; 30 }
● 输出结果,不如 multiply02 的正方形局部内存表现好,主要原因是全局内存的读取和写入时位置太分散。
Time kernel : 103 ms Result correct. Time total: 34299 ms
● 在 multiply03 的基础上,将 Bsub 也扩展为 float4 类型,四个元素分布在同一行连续位置上,与 Asub 不同,如 Bsub[0][0] == (float4)(B[0][0], B[0][1], B[0][2], B[0][3]),计算乘法时,Asub的各元素分别与 Bsub 的各元素相乘(可以使用矢量乘法加快速度),最后输出到 outputC 时再分开。由于 Bsub 的四个元素是横向相邻的,所以可以使用矢量读取和写入函数 vload4 和 vstore4(注意这两个函数的偏移量参数以 (float4 *) 为单位),所以在找准了目标数组中的位置(需要读取或写入的数组的一维索引)后,把该索引除以 4,即为偏移量。相当于一个工作项负责计算 outputC 中连续 4 列的 4 个分散行上共 16 个的元素。调用时 main.c 中使用 size_t globalSize[2] = { rowA / 4, colB / 4 }, localSize[2] = { 16, 16 }; 。
1 // multiply.cl 2 #define TILE_DIM 16 3 4 __kernel void multiply04(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB) 5 { 6 __local float4 Asub[TILE_DIM][TILE_DIM], Bsub[TILE_DIM][TILE_DIM]; // Asub 是离散的,Bsub是横向连续的 4 个元素 7 const int localRow = get_local_id(0), localCol = get_local_id(1); 8 const int groupRow = get_group_id(0) * get_local_size(0) * 4, groupCol = get_group_id(1) * get_local_size(1) * 4; 9 const int nTile = colA / TILE_DIM; 10 int t, k; 11 float4 acc[4] = { (float4)(0.f) }, tempA, tempB; 12 for (t = 0; t < nTile; t++) 13 { 14 Asub[localRow][localCol] = (float4)(inputA[(groupRow + TILE_DIM * 0 + localRow) * colA + TILE_DIM * t + localCol], 15 inputA[(groupRow + TILE_DIM * 1 + localRow) * colA + TILE_DIM * t + localCol], 16 inputA[(groupRow + TILE_DIM * 2 + localRow) * colA + TILE_DIM * t + localCol], 17 inputA[(groupRow + TILE_DIM * 3 + localRow) * colA + TILE_DIM * t + localCol]); 18 Bsub[localCol][localRow] = vload4(((TILE_DIM * t + localRow) * colB + groupCol + localCol * 4) / 4, inputB);// 向量读取 19 barrier(CLK_LOCAL_MEM_FENCE); 20 21 for (k = 0; k < TILE_DIM; k++) 22 { 23 tempA = Asub[localRow][k], tempB = Bsub[localCol][k]; 24 acc[0] += tempA.x * tempB; // 注意这里是一个标量乘以一个向量,得到一个向量 25 acc[1] += tempA.y * tempB; 26 acc[2] += tempA.z * tempB; 27 acc[3] += tempA.w * tempB; 28 } 29 barrier(CLK_LOCAL_MEM_FENCE); 30 } 31 for (k = 0; k < 4; k++) 32 vstore4(acc[k], ((groupRow + TILE_DIM * k + localRow) * colB + groupCol + localCol * 4) / 4, outputC); // 向量写入 33 }
● 输出结果,有显著进步
Time kernel : 40 ms Result correct. Time total: 32563 ms
● 作为 multiply03 和 multiply04 的一个补充,我一开始看到 multiply04 中 Asub 这么跳着取值的时候是很懵的,直到看了后面的 multiply06 才知道这么做是方便对该方法进行扩展。在写 multiply06 之前,我按自己的理解,让 Asub 不跳着取,而是类似 Bsub 那样,在竖方向上连续取 4 个元素,看看计算效率如何。调用时 main.c 使用 size_t globalSize[2] = { rowA / 4, colB / 4 }, localSize[2] = { 16, 16 }; 。
1 // multiply.cl 2 #define TILE_DIM 16 3 __kernel void multiply05(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB) 4 { 5 __local float4 Asub[TILE_DIM][TILE_DIM], Bsub[TILE_DIM][TILE_DIM]; // Asub,Bsub是连续的 4 个元素 6 const int localRow = get_local_id(0), localCol = get_local_id(1); 7 const int groupRow = get_group_id(0) * get_local_size(0) * 4, groupCol = get_group_id(1) * get_local_size(1) * 4; 8 const int nTile = colA / TILE_DIM; 9 int t, k; 10 float4 acc[4] = { (float4)(0.f) }, tempA, tempB; 11 for (t = 0; t < nTile; t++) 12 { 13 Asub[localRow][localCol] = (float4)(inputA[(groupRow + localRow * 4 + 0) * colA + TILE_DIM * t + localCol], // 改了读取位置 14 inputA[(groupRow + localRow * 4 + 1) * colA + TILE_DIM * t + localCol], 15 inputA[(groupRow + localRow * 4 + 2) * colA + TILE_DIM * t + localCol], 16 inputA[(groupRow + localRow * 4 + 3) * colA + TILE_DIM * t + localCol]); 17 Bsub[localCol][localRow] = vload4(((TILE_DIM * t + localRow) * colB + groupCol + localCol * 4) / 4, inputB); 18 barrier(CLK_LOCAL_MEM_FENCE); 19 20 for (k = 0; k < TILE_DIM; k++) // 乘法计算上没有变化 21 { 22 tempA = Asub[localRow][k], tempB = Bsub[localCol][k]; 23 acc[0] += tempA.x * tempB; 24 acc[1] += tempA.y * tempB; 25 acc[2] += tempA.z * tempB; 26 acc[3] += tempA.w * tempB; 27 } 28 barrier(CLK_LOCAL_MEM_FENCE); 29 } 30 for (k = 0; k < 4; k++) 31 vstore4(acc[k], ((groupRow + localRow * 4 + k) * colB + groupCol + localCol * 4) / 4, outputC); // 改了写入位置 32 }
● 输出结果,跟 multiply04 差不多
Time kernel : 38 ms Result correct. Time total: 33787 ms
● 高潮部分来了, multiply04 虽然使用了局部内存,利用 float4 类型的存储和内建函数,但认为它没有充分利用工作项的寄存器数量,所以最简单的改善方法就是要求每个工作项去计算 outputC 中更多的元素(multiply04 中是 4 * 4 = 16 个)。考虑将 Asub 在竖方向上扩展为原来的 nAsub 倍,将 Bsub 在横方向上扩展为原来的 nBsub 倍(为了优化访存,右边扩出来的部分以此放到原来 Bsub 的下方,看起来就像 Asub 那样在竖方向上扩展了,其实不是),一个工作项负责计算 outputC 中 4 * 4 * nAsub * nBsub 个元素,适当调整扩展倍数,可以提高计算效率。注意这里当 nAsub == nBsub == 1 时该算法退化为 multiply04。调用时main.c 中使用 size_t globalSize[2] = { rowA / 4 / 2, colB / 4 / 2}, localSize[2] = { 16, 16 }; ,其中的两个 2 分别等于 nAsub 和 nBsub。
1 // multiply.cl 2 #define TILE_DIM 16 3 #define nAsub 2 // Asub 扩展倍数 4 #define nBsub 2 // Bsub 扩展倍数 5 6 __kernel void multiply06(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB) 7 { 8 __local float4 Asub[TILE_DIM * nAsub][TILE_DIM], Bsub[TILE_DIM * nBsub][TILE_DIM];// 两个局部内存矩阵都在竖方向上扩展,但是扩展的定义不同 9 const int localRow = get_local_id(0), localCol = get_local_id(1); 10 const int groupRow = get_group_id(0) * get_local_size(0) * 4 * nAsub, groupCol = get_group_id(1) * get_local_size(1) * 4 * nBsub;// groupRow 的跨度需要调整 11 const int nTile = colA / TILE_DIM; 12 int t, k, subi, subj; 13 float4 acc[4 * nAsub][nBsub], tempA, tempB; // acc 扩展为二维 float4 数组 14 for (k = 0; k < 4 * nAsub; k++) // acc 清零 15 { 16 for (subj = 0; subj < nBsub; subj++) 17 acc[k][subj] = (float4)(0.f); 18 } 19 for (t = 0; t < nTile; t++) 20 { 21 for (subi = 0; subi < nAsub; subi++) // 对 Asub 添加一层循环,每次迭代填入 TILE_DIM * TILE_DIM 个元素 22 { 23 Asub[TILE_DIM * subi + localRow][localCol] = (float4)(inputA[(groupRow + TILE_DIM * (4 * subi + 0) + localRow) * colA + TILE_DIM * t + localCol], 24 inputA[(groupRow + TILE_DIM * (4 * subi + 1) + localRow) * colA + TILE_DIM * t + localCol], 25 inputA[(groupRow + TILE_DIM * (4 * subi + 2) + localRow) * colA + TILE_DIM * t + localCol], 26 inputA[(groupRow + TILE_DIM * (4 * subi + 3) + localRow) * colA + TILE_DIM * t + localCol]); 27 } 28 for (subj = 0; subj<nBsub; subj++) // 对 Bsub 类似 29 Bsub[TILE_DIM * subj + localRow][localCol] = vload4(((TILE_DIM * t + localRow) * colB + groupCol + TILE_DIM * 4 * subj + localCol * 4) / 4, inputB); 30 barrier(CLK_LOCAL_MEM_FENCE); 31 32 for (k = 0; k < TILE_DIM; k++) 33 { 34 for (subi = 0; subi < nAsub; subi++)// 计算时添加两层循环,分别完成 Asub 和 Bsub 中各分块的元素的乘法(比较纠结 k 和 subi/subj 哪个循环放里边比较好) 35 { 36 for (subj = 0; subj < nBsub; subj++) 37 { 38 tempA = Asub[TILE_DIM * subi + localRow][k]; 39 tempB = Bsub[TILE_DIM * subj + k][localCol]; 40 acc[4 * subi + 0][subj] += tempA.x * tempB; 41 acc[4 * subi + 1][subj] += tempA.y * tempB; 42 acc[4 * subi + 2][subj] += tempA.z * tempB; 43 acc[4 * subi + 3][subj] += tempA.w * tempB; 44 } 45 } 46 } 47 barrier(CLK_LOCAL_MEM_FENCE); 48 } 49 for (k = 0; k < 4 * nAsub; k++) // 写入时添加一层循环(实际上是添加了两层循环,但是 k 和 subi 的循环合并了,就像对 acc 清零的时候一样) 50 { 51 for (subj = 0; subj < nBsub; subj++) 52 vstore4(acc[k][subj], ((groupRow + TILE_DIM * k + localRow) * colB + groupCol + TILE_DIM * 4 * subj + localCol * 4) / 4, outputC); 53 } 54 }
● 输出结果,大有进步。当扩展倍数为(2,1)时没有明显提高(40 ms);倍数为(1,2)或(2,2)时提高最大,如下所示;倍数为(2,4),(4,2),(4,4)时与下面类似(26 ~ 28 ms);倍数为(8,4)时超出局部内存大小,clCreateKernel 返回 -45(CL_INVALID_PROGRAM_EXECUTABLE)。
Time kernel : 26 ms Result correct. Time total: 34380 ms
● 优化流水线。multiply04,multiply05,multiply06 的思想是:取第 0 块子矩阵放入局部内存 → 计算局部内存中第 0 块子矩阵的积累 acc → 取第 1 块子矩阵放入局部内存 → 计算局部内存中第 1 块子矩阵的积累 acc → ……。每次迭代都需要两个栅栏,即等待所有数据填入局部内存以及等待所有工作项完成计算。这里先考虑添加一个预读步骤,变成:预读第 0 块子矩阵到私有存储器(寄存器)→ 将预读的第 0 块子矩阵存入局部内存 → 计算局部内存中第 0 块子矩阵长度积累 acc → 预读第 1 块子矩阵到私有存储器(寄存器)→ 将预读的第 1 块子矩阵存入局部内存 → 计算局部内存积累 acc → ……,看起来暂时没有什么改善,但是注意到,在局部内存中进行计算的时候,虽然局部内存被占用了不能修改,但是寄存器可以空出来去预读下一块子矩阵,一旦计算完成,且寄存器预读完成,就可以用寄存器中预读好的数据去覆盖局部内存,相当于部分掩盖了从全局内存中读取数据的延迟,注意这时栅栏存在于 “用寄存器数据去覆盖局部内存数据” 的前后,分别用于保证 “上一个子矩阵的计算完成”,以及 “可以开始本次子矩阵的计算”。根据上面这种预读的思想,调整流水线为:预读第 0 块子矩阵到私有存储器 → 将预读的第 0 块子矩阵存入局部内存 → 预读第 1 块子矩阵到私有存储器 → 计算局部内存中第 0 块子矩阵长度积累 acc → 将预读的第 1 块子矩阵存入局部内存 → 预读第 2 块子矩阵到私有存储器 → 计算局部内存中第 1 块子矩阵长度积累 acc → ……。调用时参数同 multiply06。
1 // multiply.cl 2 #define TILE_DIM 16 3 #define nAsub 2 4 #define nBsub 2 5 6 #define PRE_LOAD(startA, startB) \ 7 { \ 8 for (subi = 0; subi < nAsub; subi++) \ 9 { \ 10 fetchA[subi].x = inputA[startA + (4 * subi + 0) * TILE_DIM * colA + localRow * colA + localCol]; \ 11 fetchA[subi].y = inputA[startA + (4 * subi + 1) * TILE_DIM * colA + localRow * colA + localCol]; \ 12 fetchA[subi].z = inputA[startA + (4 * subi + 2) * TILE_DIM * colA + localRow * colA + localCol]; \ 13 fetchA[subi].w = inputA[startA + (4 * subi + 3) * TILE_DIM * colA + localRow * colA + localCol]; \ 14 } \ 15 for (subj = 0; subj < nBsub; subj++) \ 16 fetchB[subj] = vload4((startB + localRow * colB + subj * TILE_DIM * 4 + localCol * 4) / 4, inputB); \ 17 } 18 19 #define STORE \ 20 { \ 21 barrier(CLK_LOCAL_MEM_FENCE); \ 22 for (int subi = 0; subi < nAsub; subi++) \ 23 Asub[subi * TILE_DIM + localRow][localCol] = fetchA[subi]; \ 24 for (int subj = 0; subj < nBsub; subj++) \ 25 Bsub[subj * TILE_DIM + localRow][localCol] = fetchB[subj]; \ 26 barrier(CLK_LOCAL_MEM_FENCE); \ 27 }; 28 29 #define COMPUTE \ 30 { \ 31 for(int k = 0; k < TILE_DIM; k++) \ 32 { \ 33 for (int subi = 0; subi < nAsub; subi++) \ 34 { \ 35 for (int subj = 0; subj < nBsub; subj++) \ 36 { \ 37 tempA = Asub[subi * TILE_DIM + localRow][k]; \ 38 tempB = Bsub[subj * TILE_DIM + k][localCol]; \ 39 acc[4 * subi + 0][subj] += tempA.x * tempB; \ 40 acc[4 * subi + 1][subj] += tempA.y * tempB; \ 41 acc[4 * subi + 2][subj] += tempA.z * tempB; \ 42 acc[4 * subi + 3][subj] += tempA.w * tempB; \ 43 } \ 44 } \ 45 } \ 46 } 47 48 __kernel void multiply07(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB) 49 { 50 __local float4 Asub[TILE_DIM * nAsub][TILE_DIM], Bsub[TILE_DIM * nBsub][TILE_DIM]; 51 const int localRow = get_local_id(0), localCol = get_local_id(1); 52 const int groupRow = get_group_id(0) * get_local_size(0) * 4 * nAsub, groupCol = get_group_id(1) * get_local_size(1) * 4 * nBsub; 53 const int nTile = colA / TILE_DIM; 54 int k, startA, startB, subi, subj; 55 float4 acc[4 * nAsub][nBsub], fetchA[nAsub], fetchB[nBsub], tempA, tempB; // fetchA 和 fetchB 为从 inputA 和 inputB 中预读的值 56 for (k = 0; k < 4 * nAsub; k++) 57 { 58 for (subj = 0; subj < nBsub; subj++) 59 acc[k][subj] = (float4)(0.f); 60 } 61 62 startA = groupRow * colA, startB = groupCol; 63 PRE_LOAD(startA, startB) // 从 inputA 和 inputB 中预读第一个子矩阵到 tempA 和 tempB中 64 for (; startA < groupRow + colA - TILE_DIM; startA += TILE_DIM, startB += TILE_DIM * colB)// 推进 TILE,最后一次不需要预读,循环减少一次 65 { 66 STORE // 将预读数据写入 Asub 和 Bsub 67 PRE_LOAD(startA + TILE_DIM, startB + TILE_DIM * colB) // 预读下一个子矩阵,inputA 中向右挪动,inputB 中向下挪 68 COMPUTE // 利用当前 Asub 和 Bsub 中的值计算积累 acc 69 } 70 STORE // 存储最后一个子矩阵 71 COMPUTE // 计算最后一个子矩阵的积累 acc 72 73 for (k = 0; k < 4 * nAsub; k++) // 数据写入 outputC,与以前相同 74 { 75 for (subj = 0; subj < nBsub; subj++) 76 vstore4(acc[k][subj], ((groupRow + TILE_DIM * k + localRow) * colB + groupCol + TILE_DIM * 4 * subj + localCol * 4) / 4, outputC); 77 } 78 }
● 输出结果,仍然有进步
Time kernel : 13 ms Result correct. Time total: 34598 ms
● 笔记
■ 可惜向量数据类型没有内建的点积之类的运算,不然可以把 Asub 取成横向连续 4 个元素,Bsub 取成竖向连续 4 个元素
■ 上面所有结果没有考虑 warming up 的问题。试了一下,multiply04 首次跑需要 40 ms 左右,但如果在其之后再重复跑 100 次,总共只要 2198 ms
■ 书上代码非常难看,变量名各种 ab,ae,ii,jj (我知道是 A_begin,A_end,还有用于区别 i,j),还有 bx,by,tx,ty(移植自 CUDA 的 blockIdx,threadIdx?);输入 B 和输出 C 的形参莫名其妙就变成 float4 类型了,用着还挺直率
1 tb[ty][tx] = b[j + ty * N_float4 + tx];// 这种代码看着真恶心,关键还是向量操作(索引已经从 float* 转化为 float4*)
■ 中括号记法,这是我在这次矩阵乘法编程中使用的重要方法,记录下来方便以后使用和分析,并以 multiply06 中复杂的索引计算为例进行说明。写到另一篇(http://www.cnblogs.com/cuancuancuanhao/p/9063167.html)里面去了。