OpenCL使用local内存优化矩阵乘法例子

发布时间 2024-01-09 09:21:49作者: 兜尼完

本例是俩个768×768的矩阵相乘的例子,代码来自《OpenCL异构并行计算》这本书,有修改。下文代码在VS2017和OpenCV430和OpenCL3的环境下开发和测试的,CPU型号是Intel Core i5-7400,用的是核芯显卡。代码里的kernel1是普通OpenCL代码计算乘法,kernel2是使用local内存优化的乘法。代码里也有OpenCV的矩阵乘法。总共3种方法用于对比效率。

这里详细说明kernel2优化的代码执行流程。注意本说明不是下方代码功能的准确复述,而是一个流程描述。假如矩阵A是16行32列,矩阵B是32行16列,代码中设置一个工作组是8×8的大小。下图已把每个组标记成不同的颜色。那么外层for循环是移动8×8的运算块,第一次循环时加载的是A的蓝色块数据和B的蓝色块数据,第二次循环时加载的是A的橙色块数据和B的橙色块数据,第三次循环时加载的是A的黄色块数据和B的黄色块数据……而内层for循环是计算色块内部的每一个结果,即A的色块的一行乘以B的色块中的一列。因为有barrier(...)函数,所以内层for循环会在ta和tb数组中数据填充完成之后才计算乘积。待外层for循环结束之后内层for循环则把A的一整行乘以B的一整列的结果计算出来存入矩阵C中。

矩阵A示意图

矩阵B示意图

下面是程序运行输出的结果矩阵的样子。是灰色背景,加黑色文字IOU,符合预期:

CPP文件。注意代码中str2Kernel的work-item大小是(16,16),它的值应根据实际情况设置。如果设置的值过大可能会导致核函数无法执行,从而使输出矩阵C的数据全是错的:

int main()
{
    int64 t1, t2;

    /* A是M行K列,B是K行N列,C是M行N列 */
    std::string kernel1(R"CLC(
        kernel void func1(global float* a, global float* b, int M, int N, int K, global float *c) 
        {
            int i = get_global_id(1);
            int j = get_global_id(0);
            float v = 0;
            for (int k = 0; k < K; k++)
            {
                v += a[i * K + k] * b[k * N + j];
            }
            c[i * N + j] = v;
        })CLC");

    std::string kernel2(R"CLC(
        #define BS 16
        kernel void func2(global float* a, global float* b, int M, int N, int K, global float *c)
        {
            int by = get_group_id(1);
            int bx = get_group_id(0);
            int ty = get_local_id(1); /* 0~BS */
            int tx = get_local_id(0); /* 0~BS */
            local float ta[BS][BS];
            local float tb[BS][BS];
            int ab = K * BS * by; /* a的起始行 */
            int ae = ab + K; /* a的行结尾 */
            int bb = BS * bx; /* 列号 */
            float v = 0;
            int i, j;
            for (i = ab, j = bb; i < ae; i += BS, j += BS * N)
            {
                ta[ty][tx] = a[i + ty * K + tx];
                tb[ty][tx] = b[j + ty * N + tx];
                barrier(CLK_LOCAL_MEM_FENCE);
                for (int k = 0; k < BS; k++)
                {
                    v += ta[ty][k] * tb[k][tx];
                }
                barrier(CLK_LOCAL_MEM_FENCE);
            }
            c[N * BS * by + ty * N + bx * BS + tx] = v;
        })CLC");

    std::vector<std::string> funcStrings;
    funcStrings.push_back(kernel1);
    funcStrings.push_back(kernel2);

    cl::Program multiplyProgram(funcStrings);
    cl_int result = multiplyProgram.build("-cl-std=CL2.0");
    if (result)
    {
        cl_int buildErr = CL_SUCCESS;
        auto buildInfo = multiplyProgram.getBuildInfo<CL_PROGRAM_BUILD_LOG>(&buildErr);
        for (auto &pair : buildInfo)
        {
            std::cerr << pair.second << std::endl << std::endl;
        }
        return 1;
    }

    Mat a = Mat::eye(768, 768, CV_32FC1) * 1.5;
    Mat b(768, 768, CV_32FC1, Scalar(0.5));
    Mat c(768, 768, CV_32FC1);
    cv::putText(b, "IOU", Point(200, 400), FONT_HERSHEY_PLAIN, 18, Scalar(0), 12);

    cl::Buffer ia(a.ptr<float>(0), a.ptr<float>(0) + 768 * 768, true);
    cl::Buffer ib(b.ptr<float>(0), b.ptr<float>(0) + 768 * 768, true);
    cl::Buffer ic(c.ptr<float>(0), c.ptr<float>(0) + 768 * 768, false);

    t1 = getTickCount();
    auto str1Kernel = cl::KernelFunctor<cl::Buffer, cl::Buffer, int, int, int, cl::Buffer>(multiplyProgram, "func1");
    str1Kernel(cl::EnqueueArgs(cl::NDRange(768, 768)), ia, ib, 768, 768, 768, ic);
    cl::copy(ic, c.ptr<float>(0), c.ptr<float>(0) + 768 * 768);
    t2 = getTickCount();
    qDebug() << u8"CL1(ms):" << (t2 - t1) / getTickFrequency() * 1000;
    imshow("c1", c);

    t1 = getTickCount();
    auto str2Kernel = cl::KernelFunctor<cl::Buffer, cl::Buffer, int, int, int, cl::Buffer>(multiplyProgram, "func2");
    str2Kernel(cl::EnqueueArgs(cl::NDRange(768, 768), cl::NDRange(16, 16)), ia, ib, 768, 768, 768, ic);
    cl::copy(ic, c.ptr<float>(0), c.ptr<float>(0) + 768 * 768);
    t2 = getTickCount();
    qDebug() << u8"CL2(ms):" << (t2 - t1) / getTickFrequency() * 1000;
    imshow("c2", c);

    t1 = getTickCount();
    c = a * b;
    t2 = getTickCount();
    qDebug() << u8"CV3(ms):" << (t2 - t1) / getTickFrequency() * 1000;
    imshow("c3", c);

    return 0;
}

以下是控制台输出文本。可以看出来使用local内存加速有较大作用。

CL1(ms): 43.5501
CL2(ms): 34.6158
CV3(ms): 296.618