▶ 矩阵乘法,按照书里的内容进行了几方面的优化,包括局部内存,矢量数据类型,寄存器,流水线等。

● 最直接的乘法。调用时 main.c 中使用 size_t globalSize[] = { rowA, colB }, localSize[] = { , }; 。rowA 蕴含在 get_global_id(0) 中了,不再出现在函数中,后面的几种方法也如此。

 // multiply.cl
__kernel void multiply01(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB)
{
const int row = get_global_id(), col = get_global_id();
int k;
float sum;
for (k = , sum = 0.0f; k < colA; k++)
sum += inputA[row * colA + k] * inputB[k * colB + col];
outputC[row * colB + col] = sum;
return;
}
 // main.c
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>
#include <cl.h> const int rowA = , colA = , colB = ;
//const int rowA = 128, colA = 128, colB = 128; // 测试用,刚够 multiply05 的 1 组
const char *sourceText = "D:\\Code\\OpenCL\\multiply.cl"; bool floatEq(const float a, const float b)// 相等返回 1
{
if (b == )
return fabs(a) < 0.001;
return fabs(a / b - ) < 0.001;
} int readText(const char* kernelPath, char **pcode)// 读取文本文件放入 pcode,返回字符串长度
{
FILE *fp;
int size;
//printf("<readText> File: %s\n", kernelPath);
fopen_s(&fp, kernelPath, "rb");
if (!fp)
{
printf("Open kernel file failed\n");
getchar();
exit(-);
}
if (fseek(fp, , SEEK_END) != )
{
printf("Seek end of file failed\n");
getchar();
exit(-);
}
if ((size = ftell(fp)) < )
{
printf("Get file position failed\n");
getchar();
exit(-);
}
rewind(fp);
if ((*pcode = (char *)malloc(size + )) == NULL)
{
printf("Allocate space failed\n");
getchar();
exit(-);
}
fread(*pcode, , size, fp);
(*pcode)[size] = '\0';
fclose(fp);
return size + ;
} int main()
{
int i, j, k, correct;
float *A, *B, *C, tempSum;
//char info[1024] = { 0 };
clock_t time; A = (float*)malloc(sizeof(float) * rowA * colA);
B = (float*)malloc(sizeof(float) * colA * colB);
C = (float*)malloc(sizeof(float) * rowA * colB);
srand();
for (i = ; i < rowA * colA; A[i] = rand() & 0xF, i++);
for (i = ; i < colA * colB; B[i] = rand() & 0xF, i++);
for (i = ; i < rowA * colB; C[i] = , i++); // 初始化平台到创建命令队列
cl_int status;
cl_uint nPlatform;
clGetPlatformIDs(, NULL, &nPlatform);
cl_platform_id *listPlatform = (cl_platform_id*)malloc(nPlatform * sizeof(cl_platform_id));
clGetPlatformIDs(nPlatform, listPlatform, NULL);
cl_uint nDevice;
clGetDeviceIDs(listPlatform[], CL_DEVICE_TYPE_ALL, , NULL, &nDevice);
cl_device_id *listDevice = (cl_device_id*)malloc(nDevice * sizeof(cl_device_id));
clGetDeviceIDs(listPlatform[], CL_DEVICE_TYPE_ALL, nDevice, listDevice, NULL);
cl_context context = clCreateContext(NULL, nDevice, listDevice, NULL, NULL, &status);
cl_command_queue queue = clCreateCommandQueue(context, listDevice[], NULL, &status); // 缓冲区
cl_mem bufferA = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(float) * rowA * colA, A, &status);
cl_mem bufferB = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(float) * colA * colB, B, &status);
cl_mem bufferC = clCreateBuffer(context, CL_MEM_WRITE_ONLY, sizeof(float) * rowA * colB, NULL, &status); // 程序与内核参数
char *code;
size_t codeLength = readText(sourceText, &code);
cl_program program = clCreateProgramWithSource(context, , (const char**)&code, &codeLength, &status);
status = clBuildProgram(program, nDevice, listDevice, NULL, NULL, NULL);
//clGetProgramBuildInfo(program, listDevice[0], CL_PROGRAM_BUILD_LOG, 1024, info, NULL);
//printf("\n%s\n", info);
cl_kernel kernel = clCreateKernel(program, "multiply01", &status);
clSetKernelArg(kernel, , sizeof(cl_mem), &bufferA);
clSetKernelArg(kernel, , sizeof(cl_mem), &bufferB);
clSetKernelArg(kernel, , sizeof(cl_mem), &bufferC);
clSetKernelArg(kernel, , sizeof(int), &colA);
clSetKernelArg(kernel, , sizeof(int), &colB);
size_t globalSize[] = { rowA, colB }, localSize[] = { , };// 注意不同函数需要调整工作组网格参数 // 执行内核
time = clock();
status = clEnqueueNDRangeKernel(queue, kernel, , NULL, globalSize, localSize, , NULL, NULL);
clFinish(queue);
printf("\nTime kernel : %d ms\n", clock() - time); // 返回并检查结果
clEnqueueReadBuffer(queue, bufferC, CL_TRUE, , sizeof(float) * rowA * colB, C, , NULL, NULL);
for (i = , correct = ; i < rowA && correct; i++)
{
for (j = ; j < colB && correct; j++)
{
for (k = , tempSum = 0.0f; k < colA; tempSum += A[i * colA + k] * B[k * colB + j], k++);
if (!floatEq(tempSum, C[i * colB + j]))
{
printf("Error at [%d, %d], calculation: %f, reference: %f\n", i, j, C[i*colA + j], tempSum);
correct = ;
}
}
}
if (correct)
printf("Result correct.\n");
printf("Time total: %d ms\n", clock() - time); // 释放资源
free(A);
free(B);
free(C);
free(listPlatform);
free(listDevice);
clReleaseContext(context);
clReleaseCommandQueue(queue);
clReleaseProgram(program);
clReleaseKernel(kernel);
clReleaseMemObject(bufferA);
clReleaseMemObject(bufferB);
clReleaseMemObject(bufferC);
getchar();
return ;
}

● 输出结果。纯 CPU 串行计算的版本花了 145730 ms,开 O3 后优化为 9157 ms(真是 9 秒)

Time kernel :  ms
Result correct.
Time total: ms// 时间全花在检查结果上了

● 使用局部内存优化,一个工作组负责输出 outputC 中大小为 TILE_DIM * TILE_DIM 的矩阵。每次从 inputA 和 inputB 中分别取出一个该大小的子矩阵放入局部内存中(Asub 和 Bsub),完成该部分的乘法运算,再抓取下一对子矩阵(inputA 中向右挪动,inputB 中向下挪)。调用时在 mian.c 中修改核函数名字就行

 // multiply.cl
#define TILE_DIM 16 __kernel void multiply02(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB)
{
__local float Asub[TILE_DIM][TILE_DIM], Bsub[TILE_DIM][TILE_DIM];
const int localRow = get_local_id(), localCol = get_local_id();
const int groupRow = get_group_id() * get_local_size(), groupCol = get_group_id() * get_local_size();// 当前工作项的首元素位置
const int nTile = colA / TILE_DIM; // 行程长除以块长,即需要计算的方块数
int t, k;
float acc;
for (t = , acc = 0.0f; t < nTile; t++)
{
Asub[localRow][localCol] = inputA[(groupRow + localRow) * colA + TILE_DIM * t + localCol]; // 读取块A,注意列要用 t 来算
Bsub[localCol][localRow] = inputB[(TILE_DIM * t + localRow) * colB + groupCol + localCol]; // 读取块B,注意行要用 t 来算,注意交换了 Bsub 的行列,优化访问
//Bsub[localRow][localCol] = inputB[(t * TILE_DIM + localRow) * colB + groupCol + localCol];// 不交换 Bsub 行列的情形,与上一行相互替换
barrier(CLK_LOCAL_MEM_FENCE); for (k = ; k < TILE_DIM; k++) // 在局部内存上计算累进,acc 为各工作项私有,不会有冲突
acc += Asub[localRow][k] * Bsub[localCol][k];
//acc += Asub[localRow][k] * Bsub[localCol][k]; // 不交换 Bsub 行列的情形,与上一行相互替换
barrier(CLK_LOCAL_MEM_FENCE); // 保证整个方块算完,才能进下一个方块
}
outputC[(groupRow + localRow) * colB + groupCol + localCol] = acc;
}

● 输出结果,有明显提高。不交换 Bsub 行列的情形时间稍长,约 91 ms。

Time kernel :  ms
Result correct.
Time total: ms

● 在 multiply02 的基础上,将 Asub 扩展为 float4 类型,Asub 的一个元素记录着 A 中行跨度为 TILE_DIM 的 4 个元素的值,如 Asub[0][0] == (float4)(A[0][0], A[16][0], A[32][0], A[48][0]),Bsub保持不变(为了方便后面 multiply04 的理解,这里先变A不变B),乘法计算时同时与 Bsub 相乘,最后输出到 outputC 时再分开,相当于一个工作项负责计算 outputC 中同一列中 4 个分散位置上的元素。调用时 main.c 中使用 size_t globalSize[] = { rowA / , colB }, localSize[] = { , }; 。

 // multiply.cl
#define TILE_DIM 16 __kernel void multiply03(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB)
{
__local float4 Asub[TILE_DIM][TILE_DIM]; // Asub 扩展为 float4,相当于原来的 4 倍大小
__local float Bsub[TILE_DIM][TILE_DIM]; // Bsub 保持不变
const int localRow = get_local_id(), localCol = get_local_id();
const int groupRow = get_group_id() * get_local_size() * , globalCol = get_global_id(); // 注意不同的工作组在行方向上相差为 get_local_size(0) * 4
const int nTile = colA / TILE_DIM;
int t, k;
float4 acc = (float4)(.f), temp;
for (t = ; t < nTile; t++)
{
Asub[localRow][localCol] = (float4)(inputA[(groupRow + TILE_DIM * + localRow) * colA + TILE_DIM * t + localCol], // 从 A 中离散的 4 个位置上取元素放入 Asub
inputA[(groupRow + TILE_DIM * + localRow) * colA + TILE_DIM * t + localCol],
inputA[(groupRow + TILE_DIM * + localRow) * colA + TILE_DIM * t + localCol],
inputA[(groupRow + TILE_DIM * + localRow) * colA + TILE_DIM * t + localCol]);
Bsub[localCol][localRow] = inputB[(TILE_DIM * t + localRow) * colB + globalCol];
barrier(CLK_LOCAL_MEM_FENCE); for (k = ; k < TILE_DIM; k++) // 在局部内存上计算累进,每次计算四个位置乘法
acc += Asub[localRow][k] * Bsub[localCol][k];
barrier(CLK_LOCAL_MEM_FENCE);
}
outputC[(groupRow + TILE_DIM * + localRow) * colB + globalCol] = acc.x; // 输出时分散到各位置
outputC[(groupRow + TILE_DIM * + localRow) * colB + globalCol] = acc.y;
outputC[(groupRow + TILE_DIM * + localRow) * colB + globalCol] = acc.z;
outputC[(groupRow + TILE_DIM * + localRow) * colB + globalCol] = acc.w;
}

● 输出结果,不如 multiply02 的正方形局部内存表现好,主要原因是全局内存的读取和写入时位置太分散。

Time kernel :  ms
Result correct.
Time total: ms

● 在 multiply03 的基础上,将 Bsub 也扩展为 float4 类型,但注意其取值方式与 Asub 不同。Bsub 的一个元素记录着 B 横向上相邻的 4 个元素的值,如 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[] = { rowA / , colB / }, localSize[] = { , }; 。

 // multiply.cl
#define TILE_DIM 16 __kernel void multiply04(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB)
{
__local float4 Asub[TILE_DIM][TILE_DIM], Bsub[TILE_DIM][TILE_DIM]; // Asub 是离散的,Bsub是横向连续的 4 个元素
const int localRow = get_local_id(), localCol = get_local_id();
const int groupRow = get_group_id() * get_local_size() * , groupCol = get_group_id() * get_local_size() * ;
const int nTile = colA / TILE_DIM;
int t, k;
float4 acc[] = { (float4)(.f) }, tempA, tempB;
for (t = ; t < nTile; t++)
{
Asub[localRow][localCol] = (float4)(inputA[(groupRow + TILE_DIM * + localRow) * colA + TILE_DIM * t + localCol],
inputA[(groupRow + TILE_DIM * + localRow) * colA + TILE_DIM * t + localCol],
inputA[(groupRow + TILE_DIM * + localRow) * colA + TILE_DIM * t + localCol],
inputA[(groupRow + TILE_DIM * + localRow) * colA + TILE_DIM * t + localCol]);
Bsub[localCol][localRow] = vload4(((TILE_DIM * t + localRow) * colB + groupCol + localCol * ) / , inputB);// 向量读取
barrier(CLK_LOCAL_MEM_FENCE); for (k = ; k < TILE_DIM; k++)
{
tempA = Asub[localRow][k], tempB = Bsub[localCol][k];
acc[] += tempA.x * tempB; // 注意这里是一个标量乘以一个向量,得到一个向量
acc[] += tempA.y * tempB;
acc[] += tempA.z * tempB;
acc[] += tempA.w * tempB;
}
barrier(CLK_LOCAL_MEM_FENCE);
}
for (k = ; k < ; k++)
vstore4(acc[k], ((groupRow + TILE_DIM * k + localRow) * colB + groupCol + localCol * ) / , outputC); // 向量写入
}

● 输出结果,有显著进步

Time kernel :  ms
Result correct.
Time total: ms

● 作为 multiply03 和 multiply04 的一个补充,我一开始看到 multiply04 中 Asub 这么跳着取值的时候是很懵的,直到看了后面的 multiply06 才知道这么做是方便对该方法进行扩展。在写 multiply06 之前,我按自己的理解,让 Asub 不跳着取,而是类似 Bsub 那样,在竖方向上连续取 4 个元素,看看计算效率如何。调用时 main.c 使用 size_t globalSize[] = { rowA / , colB / }, localSize[] = { , }; 。

 // multiply.cl
#define TILE_DIM 16
__kernel void multiply05(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB)
{
__local float4 Asub[TILE_DIM][TILE_DIM], Bsub[TILE_DIM][TILE_DIM]; // Asub,Bsub是连续的 4 个元素
const int localRow = get_local_id(), localCol = get_local_id();
const int groupRow = get_group_id() * get_local_size() * , groupCol = get_group_id() * get_local_size() * ;
const int nTile = colA / TILE_DIM;
int t, k;
float4 acc[] = { (float4)(.f) }, tempA, tempB;
for (t = ; t < nTile; t++)
{
Asub[localRow][localCol] = (float4)(inputA[(groupRow + localRow * + ) * colA + TILE_DIM * t + localCol], // 改了读取位置
inputA[(groupRow + localRow * + ) * colA + TILE_DIM * t + localCol],
inputA[(groupRow + localRow * + ) * colA + TILE_DIM * t + localCol],
inputA[(groupRow + localRow * + ) * colA + TILE_DIM * t + localCol]);
Bsub[localCol][localRow] = vload4(((TILE_DIM * t + localRow) * colB + groupCol + localCol * ) / , inputB);
barrier(CLK_LOCAL_MEM_FENCE); for (k = ; k < TILE_DIM; k++) // 乘法计算上没有变化
{
tempA = Asub[localRow][k], tempB = Bsub[localCol][k];
acc[] += tempA.x * tempB;
acc[] += tempA.y * tempB;
acc[] += tempA.z * tempB;
acc[] += tempA.w * tempB;
}
barrier(CLK_LOCAL_MEM_FENCE);
}
for (k = ; k < ; k++)
vstore4(acc[k], ((groupRow + localRow * + k) * colB + groupCol + localCol * ) / , outputC); // 改了写入位置
}

● 输出结果,跟 multiply04 差不多

Time kernel :  ms
Result correct.
Time total: 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[] = { rowA / / , colB / / }, localSize[] = { , }; ,其中的两个 2 分别等于 nAsub 和 nBsub。

 // multiply.cl
#define TILE_DIM 16
#define nAsub 2 // Asub 扩展倍数
#define nBsub 2 // Bsub 扩展倍数 __kernel void multiply06(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB)
{
__local float4 Asub[TILE_DIM * nAsub][TILE_DIM], Bsub[TILE_DIM * nBsub][TILE_DIM];// 两个局部内存矩阵都在竖方向上扩展,但是扩展的定义不同
const int localRow = get_local_id(), localCol = get_local_id();
const int groupRow = get_group_id() * get_local_size() * * nAsub, groupCol = get_group_id() * get_local_size() * * nBsub;// groupRow 的跨度需要调整
const int nTile = colA / TILE_DIM;
int t, k, subi, subj;
float4 acc[ * nAsub][nBsub], tempA, tempB; // acc 扩展为二维 float4 数组
for (k = ; k < * nAsub; k++) // acc 清零
{
for (subj = ; subj < nBsub; subj++)
acc[k][subj] = (float4)(.f);
}
for (t = ; t < nTile; t++)
{
for (subi = ; subi < nAsub; subi++) // 对 Asub 添加一层循环,每次迭代填入 TILE_DIM * TILE_DIM 个元素
{
Asub[TILE_DIM * subi + localRow][localCol] = (float4)(inputA[(groupRow + TILE_DIM * ( * subi + ) + localRow) * colA + TILE_DIM * t + localCol],
inputA[(groupRow + TILE_DIM * ( * subi + ) + localRow) * colA + TILE_DIM * t + localCol],
inputA[(groupRow + TILE_DIM * ( * subi + ) + localRow) * colA + TILE_DIM * t + localCol],
inputA[(groupRow + TILE_DIM * ( * subi + ) + localRow) * colA + TILE_DIM * t + localCol]);
}
for (subj = ; subj<nBsub; subj++) // 对 Bsub 类似
Bsub[TILE_DIM * subj + localRow][localCol] = vload4(((TILE_DIM * t + localRow) * colB + groupCol + TILE_DIM * * subj + localCol * ) / , inputB);
barrier(CLK_LOCAL_MEM_FENCE); for (k = ; k < TILE_DIM; k++)
{
for (subi = ; subi < nAsub; subi++)// 计算时添加两层循环,分别完成 Asub 和 Bsub 中各分块的元素的乘法(比较纠结 k 和 subi/subj 那个循环放里边比较好)
{
for (subj = ; subj < nBsub; subj++)
{
tempA = Asub[TILE_DIM * subi + localRow][k];
tempB = Bsub[TILE_DIM * subj + k][localCol];
acc[ * subi + ][subj] += tempA.x * tempB;
acc[ * subi + ][subj] += tempA.y * tempB;
acc[ * subi + ][subj] += tempA.z * tempB;
acc[ * subi + ][subj] += tempA.w * tempB;
}
}
}
barrier(CLK_LOCAL_MEM_FENCE);
}
for (k = ; k < * nAsub; k++) // 写入时添加一层循环(实际上是添加了两层循环,但是 k 和 subi 的循环合并了,就像对 acc 清零的时候一样)
{
for (subj = ; subj < nBsub; subj++)
vstore4(acc[k][subj], ((groupRow + TILE_DIM * k + localRow) * colB + groupCol + TILE_DIM * * subj + localCol * ) / , outputC);
}
}

● 输出结果,大有进步。当扩展倍数为(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 :  ms
Result correct.
Time total: ms

● 优化流水线。multiply04,multiply05,multiply06 的思想是:取第 0 块子矩阵放入局部内存 → 计算局部内存中第 0 块子矩阵的积累 acc → 取第 1 块子矩阵放入局部内存 → 计算局部内存中第 1 块子矩阵的积累 acc → ……。每次迭代都需要两个栅栏,即等待所有数据填入局部内存以及等待所有工作项完成计算。这里先考虑添加一个预读步骤,变成:预读第 0 块子矩阵到私有存储器(寄存器) → 将预读的第 0 块子矩阵存入局部内存 → 计算局部内存中第 0 块子矩阵长度积累 acc → 预读取第 1 块子矩阵放入局部内存 → 计算局部内存积累 acc → ……,看起来暂时没有什么改善,但是注意到,在局部内存中进行计算的时候,虽然局部内存被占用了不能修改,但是寄存器可以空出来去预读下一块子矩阵,一旦计算完成,且寄存器预读完成,就可以把寄存器中预读好的数据覆盖局部内存,相当于部分掩盖了从全局内存中读取数据的延迟,注意这时栅栏存在于 “从寄存器数据覆盖局部内存数据” 的前后,分别用于保证 “上一个子矩阵的计算完成”,以及 “可以开始本次子矩阵的计算”。根据上面这种预读的思想,调整流水线为:预读第 0 块子矩阵到私有存储器 → 将预读的第 0 块子矩阵存入局部内存 → 预读第 1 块子矩阵到私有存储器 → 计算局部内存中第 0 块子矩阵长度积累 acc  → 将预读的第 1 块子矩阵存入局部内存 → 预读第 2 块子矩阵到私有存储器 → 计算局部内存中第 1 块子矩阵长度积累 acc → ……。调用时参数同 multiply06。

 // multiply.cl
#define TILE_DIM 16
#define nAsub 2
#define nBsub 2 #define PRE_LOAD(startA, startB) \
{ \
for (subi = ; subi < nAsub; subi++) \
{ \
fetchA[subi].x = inputA[startA + ( * subi + ) * TILE_DIM * colA + localRow * colA + localCol]; \
fetchA[subi].y = inputA[startA + ( * subi + ) * TILE_DIM * colA + localRow * colA + localCol]; \
fetchA[subi].z = inputA[startA + ( * subi + ) * TILE_DIM * colA + localRow * colA + localCol]; \
fetchA[subi].w = inputA[startA + ( * subi + ) * TILE_DIM * colA + localRow * colA + localCol]; \
} \
for (subj = ; subj < nBsub; subj++) \
fetchB[subj] = vload4((startB + localRow * colB + subj * TILE_DIM * + localCol * ) / , inputB); \
} #define STORE \
{ \
barrier(CLK_LOCAL_MEM_FENCE); \
for (int subi = ; subi < nAsub; subi++) \
Asub[subi * TILE_DIM + localRow][localCol] = fetchA[subi]; \
for (int subj = ; subj < nBsub; subj++) \
Bsub[subj * TILE_DIM + localRow][localCol] = fetchB[subj]; \
barrier(CLK_LOCAL_MEM_FENCE); \
}; #define COMPUTE \
{ \
for(int k = ; k < TILE_DIM; k++) \
{ \
for (int subi = ; subi < nAsub; subi++) \
{ \
for (int subj = ; subj < nBsub; subj++) \
{ \
tempA = Asub[subi * TILE_DIM + localRow][k]; \
tempB = Bsub[subj * TILE_DIM + k][localCol]; \
acc[ * subi + ][subj] += tempA.x * tempB; \
acc[ * subi + ][subj] += tempA.y * tempB; \
acc[ * subi + ][subj] += tempA.z * tempB; \
acc[ * subi + ][subj] += tempA.w * tempB; \
} \
} \
} \
} __kernel void multiply07(__global float *inputA, __global float *inputB, __global float *outputC, int colA, int colB)
{
__local float4 Asub[TILE_DIM * nAsub][TILE_DIM], Bsub[TILE_DIM * nBsub][TILE_DIM];
const int localRow = get_local_id(), localCol = get_local_id();
const int groupRow = get_group_id() * get_local_size() * * nAsub, groupCol = get_group_id() * get_local_size() * * nBsub;
const int nTile = colA / TILE_DIM;
int k, startA, startB, subi, subj;
float4 acc[ * nAsub][nBsub], fetchA[nAsub], fetchB[nBsub], tempA, tempB; // fetchA 和 fetchB 为从 inputA 和 inputB 中预读的值
for (k = ; k < * nAsub; k++)
{
for (subj = ; subj < nBsub; subj++)
acc[k][subj] = (float4)(.f);
} startA = groupRow * colA, startB = groupCol;
PRE_LOAD(startA, startB)// 从 inputA 和 inputB 中预读第一个子矩阵到 tempA 和 tempB中
for (; startA < groupRow + colA - TILE_DIM; startA += TILE_DIM, startB += TILE_DIM * colB)// 推进 TILE,最后一次不需要预读,循环减少一次
{
STORE // 将预读数据写入 Asub 和 Bsub
PRE_LOAD(startA + TILE_DIM, startB + TILE_DIM * colB) // 预读下一个子矩阵,inputA 中向右挪动,inputB 中向下挪
COMPUTE // 利用当前 Asub 和 Bsub 中的值计算积累 acc
}
STORE // 存储最后一个子矩阵
COMPUTE // 计算最后一个子矩阵的积累 acc for (k = ; k < * nAsub; k++) // 数据写入 outputC,与以前相同
{
for (subj = ; subj < nBsub; subj++)
vstore4(acc[k][subj], ((groupRow + TILE_DIM * k + localRow) * colB + groupCol + TILE_DIM * * subj + localCol * ) / , outputC);
}
}

● 输出结果,仍然有进步

Time kernel :  ms
Result correct.
Time total: 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 类型了,用着还挺直率

 tb[ty][tx] = b[j + ty * N_float4 + tx];// 这种代码看着真恶心,关键还是向量操作(索引已经从 float* 转化为 float4*)

■ 中括号记法,这是我在这次矩阵乘法编程中使用的重要方法,记录下来方便以后使用和分析,并以 multiply06 中复杂的索引计算为例进行说明。写到另一篇(http://www.cnblogs.com/cuancuancuanhao/p/9063167.html)里面去了。

OpenCL 矩阵乘法的更多相关文章

  1. CUDA 矩阵乘法终极优化指南

    作者:马骏 | 旷视 MegEngine 架构师 前言 单精度矩阵乘法(SGEMM)几乎是每一位学习 CUDA 的同学绕不开的案例,这个经典的计算密集型案例可以很好地展示 GPU 编程中常用的优化技巧 ...

  2. *HDU2254 矩阵乘法

    奥运 Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Total Submissi ...

  3. *HDU 1757 矩阵乘法

    A Simple Math Problem Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Ot ...

  4. CH Round #30 摆花[矩阵乘法]

    摆花 CH Round #30 - 清明欢乐赛 背景及描述 艺术馆门前将摆出许多花,一共有n个位置排成一排,每个位置可以摆花也可以不摆花.有些花如果摆在相邻的位置(隔着一个空的位置不算相邻),就不好看 ...

  5. POJ3070 Fibonacci[矩阵乘法]

    Fibonacci Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 13677   Accepted: 9697 Descri ...

  6. bzoj 2738 矩阵乘法

    其实这题跟矩阵乘法没有任何卵关系,直接整体二分,用二维树状数组维护(刚刚学会>_<),复杂度好像有点爆炸(好像有十几亿不知道是不是算错了),但我们不能怂啊23333. #include&l ...

  7. 【BZOJ-2476】战场的数目 矩阵乘法 + 递推

    2476: 战场的数目 Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 58  Solved: 38[Submit][Status][Discuss] D ...

  8. 【BZOJ-1898】Swamp 沼泽鳄鱼 矩阵乘法

    1898: [Zjoi2005]Swamp 沼泽鳄鱼 Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 1012  Solved: 566[Submit][S ...

  9. 【Codeforces718C】Sasha and Array 线段树 + 矩阵乘法

    C. Sasha and Array time limit per test:5 seconds memory limit per test:256 megabytes input:standard ...

随机推荐

  1. Python的内置方法

    一 isinstance(obj,cls)和issubclass(sub,super) isinstance(obj,cls)检查是否obj是否是类 cls 的对象 class Foo(object) ...

  2. SQLServer 账户当前被锁定

    嗯,被攻击了一波,烦躁很 ‘帐户当前被锁定,所以用户 ‘sa’ 登录失败.系统管理员无法将该帐户解锁’解决方法 如果短时间内不停连接,就会被SQL SERVER误认为是这是攻击,会将此账号锁定. 要用 ...

  3. 芯灵思Sinlinx A64开发板设置qt程序自启动

    开发平台 芯灵思Sinlinx A64 内存: 1GB 存储: 4GB 开发板详细参数 https://m.tb.cn/h.3wMaSKm 对于开发板开机启动程序的设置可以这样做通过串口连接开发板 v ...

  4. 福利之——如何写好年终总结PPT

    撰写年终总结PPT大致分三个步骤(TDR三部曲): 第一步:确定大纲(THINK) 第二步:优化PPT(DESIGN) 第三步:练习演讲(REHEARSE) 让领导看见你的成绩,听明白你未来的‘宏伟计 ...

  5. Freescale MKL16Z1288VF4 芯片调试接口

    WDOG监视内部系统操作,并在发生故障时强制复位.它可以运行在一个独立的1 kHz低功率振荡器,具有可编程刷新窗口,以检测程序流或系统频率的偏差. 看门狗计时器保持一个时间在系统上运行,并重置它,以防 ...

  6. voc-fcn-alexnet网络结构理解

    一.写在前面 fcn是首次使用cnn来实现语义分割的,论文地址:fully convolutional networks for semantic segmentation 实现代码地址:https: ...

  7. C# .NET 实体类转Dictionary

    -- System.Reflection.PropertyInfo[] cfgItemProperties = cfgItem.GetType().GetProperties(System.Refle ...

  8. 工控随笔_09_西门子_S7-200 Smart与V20 USS通信USS_RPM_R利用轮询的方式通讯异常

    前两天处理过一个故障,是S7-200 Smart与V20的USS通讯,设备厂家在程序里面利 用USS_RPM _R程序循环轮询5个V20设备读取频率和电流值等信息. 图 USS_RPM_R读取信息 上 ...

  9. 如何找出单链表中的倒数第k个元素

    方法一:快慢指针法 在查找过程中,设置两个指针,初始时指向首元结点(第一个元素结点). 然后,让其中一个指针先前移k步. 然后两个指针再同时往前移动.当先行的指针值为NULL时,另一个指针所指的位置就 ...

  10. 总结,为什么要重写hashset的hashcode()和equals()?

    看了非常多博客,怕自己忘记了,通俗易懂的总结如下 本人总结下: 重写前,比较地址,hashcode方法如果相等可能是同一个对象,所以再用equals再比内存地址 重写后,比较值,重写hashCode方 ...