OpenCL 计算与顺序算法的输出不匹配

Posted

技术标签:

【中文标题】OpenCL 计算与顺序算法的输出不匹配【英文标题】:OpenCL computation does not match output of sequential algorithm 【发布时间】:2014-10-27 22:40:40 【问题描述】:

我正在尝试在 OpenCL 中实现 LU 分解的简单版本。首先,我在 C++ 中实现了一个顺序版本,并构造了方法来验证我的结果(即乘法方法)。接下来,我在内核中实现了我的算法,并使用手动验证的输入(即 5x5 矩阵)对其进行了测试。这很好用。

但是,当我在大于 5x5 的随机生成矩阵上运行我的算法时,我得到了奇怪的结果。我已经清理了我的代码,手动检查了计算,但我无法弄清楚我的内核哪里出了问题。我开始认为这可能与floats 和计算的稳定性有关。我的意思是误差范围被传播并变得越来越大。我很清楚我可以交换行以获得最大的枢轴值等,但有时误差幅度会很大。在任何情况下,我都希望结果 - 尽管是错误的 - 与顺序算法相同。我需要一些帮助来确定我可能做错了什么。

我使用的是一维数组,因此使用二维矩阵寻址的过程如下:

A(row, col) = A[row * matrix_width + col].

关于结果,我可能会补充一点,我决定将 L 和 U 矩阵合并为一个。所以给定 L 和 U:

L:     U:
1 0 0  A B C
X 1 0  0 D E
Y Z 1  0 0 F

我将它们显示为:

A:
A B C
X D E
Y Z F

内核如下:

参数source是我要分解的原始矩阵。 参数destin 是目的地。 matrix_size 是矩阵的总大小(对于 3x3 矩阵来说是 9),matrix_width 是宽度(对于 3x3 矩阵来说是 3)。

__kernel void matrix(
    __global float * source,
    __global float * destin,
    unsigned int   matrix_size,
    unsigned int   matrix_width
    )

    unsigned int index = get_global_id(0);
    int col_idx = index % matrix_width;
    int row_idx = index / matrix_width;

    if (index >= matrix_size)
        return;

    // First of all, copy our value to the destination.
    destin[index] = source[index];
    // Iterate over all the pivots.
    for(int piv_idx = 0; piv_idx < matrix_width; piv_idx++)
    
        // We have to be the row below the pivot row
        // And we have to be the column of the pivot
        // or right of that column.
        if(col_idx < piv_idx || row_idx <= piv_idx)
            return;
        // Calculate the divisor.
        float pivot_value       = destin[(piv_idx * matrix_width) + piv_idx];
        float below_pivot_value = destin[(row_idx * matrix_width) + piv_idx];
        float divisor           = below_pivot_value/ pivot_value;

        // Get the value in the pivot row on this column.
        float pivot_row_value = destin[(piv_idx * matrix_width) + col_idx];
        float current_value   = destin[index];
        destin[index]         = current_value - (pivot_row_value * divisor);


        // Write the divisor to the memory (we won't use these values anymore!)
        // if we are the value under the pivot.
        barrier(CLK_GLOBAL_MEM_FENCE);
        if(col_idx == piv_idx)
        
            int divisor_location = (row_idx * matrix_width) + piv_idx;
            destin[divisor_location] = divisor;
        
        barrier(CLK_GLOBAL_MEM_FENCE);
    

这是顺序版本:

// Decomposes a matrix into L and U but in the same matrix.
float * decompose(float* A, int matrix_width)

    int total_length = matrix_width*matrix_width;
    float *U = new float[total_length];

    for (int i = 0; i < total_length; i++)
    
        U[i] = A[i];
    
    for (int row = 0; row < matrix_width; row++)
    
        int pivot_idx = row;
        float pivot_val = U[pivot_idx * matrix_width + pivot_idx];

        for (int r = row + 1; r < matrix_width; r++)
        
            float below_pivot = U[r*matrix_width + pivot_idx];
            float divisor = below_pivot / pivot_val;

            for (int row_idx = pivot_idx; row_idx < matrix_width; row_idx++)
            
                float value = U[row * matrix_width + row_idx];
                U[r*matrix_width + row_idx] = U[r*matrix_width + row_idx] - (value * divisor);
            
            U[r * matrix_width + pivot_idx] = divisor;
        
    
    return U;

我得到的示例输出如下:

Workgroup size: 1
Array dimension: 6
Original unfactorized:
|     176.000000 |     133.000000 |     431.000000 |     839.000000 |     739.000000 |     450.000000 |
|     507.000000 |     718.000000 |     670.000000 |     753.000000 |     122.000000 |     941.000000 |
|     597.000000 |     449.000000 |     596.000000 |     742.000000 |     491.000000 |     212.000000 |
|     159.000000 |     944.000000 |     797.000000 |     717.000000 |     822.000000 |     219.000000 |
|     266.000000 |     755.000000 |      33.000000 |     231.000000 |     824.000000 |     785.000000 |
|     724.000000 |     408.000000 |     652.000000 |     863.000000 |     663.000000 |     113.000000 |
Sequential:
|     176.000000 |     133.000000 |     431.000000 |     839.000000 |     739.000000 |     450.000000 |
|       2.880682 |     334.869324 |    -571.573853 |   -1663.892090 |   -2006.823730 |    -355.306763 |
|       3.392045 |      -0.006397 |    -869.627747 |   -2114.569580 |   -2028.558716 |   -1316.693359 |
|       0.903409 |       2.460203 |      -2.085742 |    -357.893066 |     860.526367 |   -2059.689209 |
|       1.511364 |       1.654343 |      -0.376231 |      -2.570729 |    4476.049805 |   -5097.599121 |
|       4.113636 |      -0.415427 |       1.562076 |      -0.065806 |       0.003290 |      52.263515 |
Sequential multiplied matching with original?:
1
GPU:
|     176.000000 |     133.000000 |     431.000000 |     839.000000 |     739.000000 |     450.000000 |
|       2.880682 |     334.869293 |    -571.573914 |   -1663.892212 |   -2006.823975 |    -355.306885 |
|       3.392045 |      -0.006397 |    -869.627808 |   -2114.569580 |   -2028.558716 |   -1316.693359 |
|       0.903409 |       2.460203 |      -2.085742 |    -357.892578 |    5091.575684 |   -2059.688965 |
|       1.511364 |       1.654343 |      -0.376232 |      -2.570732 |   16116.155273 |   -5097.604980 |
|       4.113636 |      -0.415427 |      -0.737347 |       2.005755 |      -3.655331 |    -237.480438 |
GPU multiplied matching with original?:
Values differ: 5053.05 -- 822
0
Values differ: 5091.58 -- 860.526
Correct solution? 0

编辑

好的,我想我明白为什么它以前不起作用了。原因是我只在每个工作组上同步。当我使用等于矩阵中项目数的工作组大小来调用我的内核时,它总是正确的,因为这样障碍就会正常工作。但是,我决定采用 cmets 中提到的方法。将多个内核排入队列并等待每个内核完成,然后再启动下一个内核。然后,这将映射到矩阵每一行的迭代,并将其与枢轴元素相乘。这确保我不会修改或读取当时内核正在修改的元素。

同样,这适用于小矩阵。所以我认为我假设它只是同步是错误的。根据 Baiz 的要求,我在此处发布了调用内核的整个 main

int main(int argc, char *argv[])

    try 
        if (argc != 5) 
            std::ostringstream oss;
            oss << "Usage: " << argv[0] << " <kernel_file> <kernel_name> <workgroup_size> <array width>";
            throw std::runtime_error(oss.str());
        
        // Read in arguments.
        std::string kernel_file(argv[1]);
        std::string kernel_name(argv[2]);
        unsigned int workgroup_size = atoi(argv[3]);
        unsigned int array_dimension = atoi(argv[4]);
        int total_matrix_length = array_dimension * array_dimension;
        // Print parameters
        std::cout << "Workgroup size: " << workgroup_size << std::endl;
        std::cout << "Array dimension: " << array_dimension << std::endl;

        // Create matrix to work on.
        // Create a random array.
        int matrix_width = sqrt(total_matrix_length);
        float* input_matrix = new float[total_matrix_length];
        input_matrix = randomMatrix(total_matrix_length);

        /// Debugging
        //float* input_matrix = new float[9];
        //int matrix_width = 3;
        //total_matrix_length = matrix_width * matrix_width;
        //input_matrix[0] = 10; input_matrix[1] = -7; input_matrix[2] = 0;
        //input_matrix[3] = -3; input_matrix[4] =  2; input_matrix[5] = 6;
        //input_matrix[6] =  5; input_matrix[7] = -1; input_matrix[8] = 5;

        // Allocate memory on the host and populate source
        float *gpu_result = new float[total_matrix_length];


        // OpenCL initialization
        std::vector<cl::Platform> platforms;
        std::vector<cl::Device> devices;
        cl::Platform::get(&platforms);
        platforms[0].getDevices(CL_DEVICE_TYPE_GPU, &devices);
        cl::Context context(devices);
        cl::CommandQueue queue(context, devices[0], CL_QUEUE_PROFILING_ENABLE);

        // Load the kernel source.
        std::string file_text;
        std::ifstream file_stream(kernel_file.c_str());
        if (!file_stream) 
            std::ostringstream oss;
            oss << "There is no file called " << kernel_file;
            throw std::runtime_error(oss.str());
        
        file_text.assign(std::istreambuf_iterator<char>(file_stream), std::istreambuf_iterator<char>());

        // Compile the kernel source.
        std::string source_code = file_text;
        std::pair<const char *, size_t> source(source_code.c_str(), source_code.size());
        cl::Program::Sources sources;
        sources.push_back(source);
        cl::Program program(context, sources);
        try 
            program.build(devices);
        
        catch (cl::Error& e) 
            std::string msg;
            program.getBuildInfo<std::string>(devices[0], CL_PROGRAM_BUILD_LOG, &msg);
            std::cerr << "Your kernel failed to compile" << std::endl;
            std::cerr << "-----------------------------" << std::endl;
            std::cerr << msg;
            throw(e);
        

        // Allocate memory on the device
        cl::Buffer source_buf(context, CL_MEM_READ_ONLY, total_matrix_length*sizeof(float));
        cl::Buffer dest_buf(context, CL_MEM_WRITE_ONLY, total_matrix_length*sizeof(float));

        // Create the actual kernel.
        cl::Kernel kernel(program, kernel_name.c_str());

        // transfer source data from the host to the device
        queue.enqueueWriteBuffer(source_buf, CL_TRUE, 0, total_matrix_length*sizeof(float), input_matrix);

        for (int pivot_idx = 0; pivot_idx < matrix_width; pivot_idx++)
        
            // set the kernel arguments
            kernel.setArg<cl::Memory>(0, source_buf);
            kernel.setArg<cl::Memory>(1, dest_buf);
            kernel.setArg<cl_uint>(2, total_matrix_length);
            kernel.setArg<cl_uint>(3, matrix_width);
            kernel.setArg<cl_int>(4, pivot_idx);

            // execute the code on the device
            std::cout << "Enqueueing new kernel for " << pivot_idx << std::endl;
            cl::Event evt;
            queue.enqueueNDRangeKernel(kernel, cl::NullRange, cl::NDRange(total_matrix_length), cl::NDRange(workgroup_size), 0, &evt);
            evt.wait();
            std::cout << "Iteration " << pivot_idx << " done" << std::endl;
        

        // transfer destination data from the device to the host
        queue.enqueueReadBuffer(dest_buf, CL_TRUE, 0, total_matrix_length*sizeof(float), gpu_result);

        // Calculate sequentially.
        float* sequential = decompose(input_matrix, matrix_width);

        // Print out the results.
        std::cout << "Sequential:\n";
        printMatrix(total_matrix_length, sequential);

        // Print out the results.
        std::cout << "GPU:\n";
        printMatrix(total_matrix_length, gpu_result);

        std::cout << "Correct solution? " << equalMatrices(gpu_result, sequential, total_matrix_length);


        // compute the data throughput in GB/s
        //float throughput = (2.0*total_matrix_length*sizeof(float)) / t; // t is in nano seconds
        //std::cout << "Achieved throughput: " << throughput << std::endl;

        // Cleanup
        // Deallocate memory
        delete[] gpu_result;
        delete[] input_matrix;
        delete[] sequential;
        return 0;
    
    catch (cl::Error& e) 
        std::cerr << e.what() << ": " << jc::readable_status(e.err());
        return 3;
    
    catch (std::exception& e) 
        std::cerr << e.what() << std::endl;
        return 2;
    
    catch (...) 
        std::cerr << "Unexpected error. Aborting!\n" << std::endl;
        return 1;
    

【问题讨论】:

我添加了一个可能的解决方案。如果可行,请告诉我。 【参考方案1】:

正如 maZZZu 所述,由于工作项的并行执行,您无法确定数组中的元素是否已被读取/写入。 这可以使用

来确保
CLK_LOCAL_MEM_FENCE/CLK_GLOBAL_MEM_FENCE

但是,这些机制仅适用于同一工作组内的线程。 无法同步来自不同工作组的工作项。

您的问题很可能是:

您将多个工作组用于最可能只能由单个工作组执行的算法 你没有使用足够的障碍

如果您已经只使用一个工作组,请尝试添加一个

屏障(CLK_GLOBAL_MEM_FENCE);

到您从/到目的地读/写的所有部分。

你应该重构你的算法:

只有一个工作组在您的矩阵上执行算法 使用本地内存以获得更好的性能(因为您重复访问元素) 在各处使用屏障。如果算法有效,您可以在解决后开始删除它们,哪些是您不需要的。

您能否发布您的内核调用和工作大小?

编辑:

根据您的算法,我想出了这段代码。 我还没有测试过它,我怀疑它会立即起作用。 但它应该可以帮助您理解如何并行化顺序算法。 它将仅通过一次内核启动来分解矩阵。

一些限制:

此代码仅适用于单个工作组。 它仅适用于大小不超过您的最大本地工作组大小(可能在 256 和 1024 之间)的矩阵。 如果你想改变它,你应该重构算法,只使用与矩阵宽度一样多的工作项。

只需将它们调整为您的 kernel.setArg(...) 代码

int nbElements = width*height;
clSetKernelArg (kernel, 0, sizeof(A), &A);
clSetKernelArg (kernel, 1, sizeof(U), &U);
clSetKernelArg (kernel, 2, sizeof(float) * widthMat * heightMat, NULL); // Local memory
clSetKernelArg (kernel, 3, sizeof(int), &width);
clSetKernelArg (kernel, 4, sizeof(int), &height);
clSetKernelArg (kernel, 5, sizeof(int), &nbElements);

内核代码:

inline int indexFrom2d(const int u, const int v, const int width)

    return width*v + u;


kernel void decompose(global float* A, 
                      global float* U,
                      local float* localBuffer, 
                      const int widthMat,
                      const int heightMat,
                      const int nbElements)

    int gidx = get_global_id(0);
    int col = gidx%widthMat;
    int row = gidx/widthMat;

    if(gidx >= nbElements)
        return;

    // Copy from global to local memory
    localBuffer[gidx] = A[gidx];

    // Sync copy process
    barrier(CLK_LOCAL_MEM_FENCE);

    for (int rowOuter = 0; rowOuter < widthMat; ++rowOuter)
    
        int pivotIdx = rowOuter;

        float pivotValue = localBuffer[indexFrom2d(pivotIdx, pivotIdx, widthMat)];

        // Data for all work items in the row
        float belowPrivot = localBuffer[indexFrom2d(pivotIdx, row, widthMat)];          
        float divisor = belowPrivot / pivotValue;

        float value = localBuffer[indexFrom2d(col, rowOuter, widthMat)];

        // Only work items below pivot and from pivot to the right
        if( widthMat > col >= pivotIdx &&
            heightMat > row >= pivotIdx + 1)
        
            localBuffer[indexFrom2d(col, row, widthMat)] = localBuffer[indexFrom2d(col, row, widthMat)] - (value * divisor);

            if(col == pivotIdx)
                localBuffer[indexFrom2d(pivotIdx, row, widthMat)] = divisor;
        
        barrier(CLK_LOCAL_MEM_FENCE);
    

    // Write back to global memory
    U[gidx] = localBuffer[gidx];

【讨论】:

要全局同步(跨所有工作项)使用多个内核。【参考方案2】:

错误太大了,不可能由浮点运算引起。

如果对您的算法没有更深入的了解,我会说问题在于您使用的是目标缓冲区中的值。使用顺序代码这很好,因为您知道那里有什么值。但是对于 OpenCL,内核是并行执行的。所以你无法判断另一个内核是否已经将它的值存储到目标缓冲区。

【讨论】:

以上是关于OpenCL 计算与顺序算法的输出不匹配的主要内容,如果未能解决你的问题,请参考以下文章

在多 GPU 系统中,给定 PCI 供应商、设备和总线 ID,如何将 OpenCL 设备与特定 GPU 匹配?

顺序串— KMP模式匹配算法-1(补充)

顺序栈练习题

为什么图表中的列顺序与查询结果集中的列顺序不匹配?

想找一个解决两个字符串匹配程度的算法。

每日算法刷题Day4-完全数分情况输出平方矩阵斐波那契数列匹配输出