【问题标题】:CUDA: Understanding the behavior of variables in the registers file in a loop with a dot product exampleCUDA:通过点积示例在循环中了解寄存器文件中变量的行为
【发布时间】:2019-02-12 03:52:15
【问题描述】:

我对 CUDA 编程非常陌生。目前我很难理解以下程序的行为来计算两个向量的点积。

点积内核dotProd 计算每个元素的乘积,并将结果缩减为长度为blockDim.x*gridDim.x 的较短向量。然后将向量 *out 中的结果复制回 Host 以进一步减少。

第二个版本,dotProdWithSharedMem 是从 CUDA By Example 一书中复制而来的,请参阅 here

我的问题是:

  1. 当内核用足够多的线程(nThreadsPerBlock*nblocks >= vector_length)启动时,dotProd的结果与CPU计算的结果一致,但dotProdWithSharedMem的结果与两者不同。可能的原因是什么? $ dot_prod.o 17 512 的可能输出:
    Number of threads per block : 256 
    Number of blocks in the grid: 512 
    Total number of threads     : 131072 
    Length of vectors           : 131072 

    GPU using registers: 9.6904191971, time consummed: 0.56154 ms
    GPU using shared   : 9.6906833649, time consummed: 0.04473 ms
    CPU result         : 9.6904191971, time consummed: 0.28504 ms
  1. 当内核启动时没有足够的线程 (nThreadsPerBlock*nblocks < vector_length),GPU 结果似乎不太准确。然而while 循环应该可以处理这个问题。我猜循环中的寄存器变量temp 可能发生了一些事情,否则结果应该与问题1 中的相同。$ dot_prod.o 17 256 的可能输出:
Number of threads per block : 256 
Number of blocks in the grid: 256 
Total number of threads     : 65536 
Length of vectors           : 131072 

GPU using registers: 9.6906890869, time consummed: 0.31478 ms
GPU using shared   : 9.6906604767, time consummed: 0.03530 ms
CPU result         : 9.6904191971, time consummed: 0.28404 ms
  1. 我不太明白dotProdWithSharedMemcache 的大小。为什么它是nThreadsPerBlock元素而不是线程总数nThreadsPerBlock * nblocks?我认为这应该是正确数量的 temp 值,对吗?

代码:

#include <iostream>
#include <string>
#include <cmath>
#include <chrono>
#include <cuda.h>


#define PI (float) 3.141592653589793

const size_t nThreadsPerBlock = 256;


static void HandleError(cudaError_t err, const char *file, int line )
{
    if (err != cudaSuccess) {
    printf( "%s in %s at line %d\n", cudaGetErrorString( err ),
            file, line );
    exit( EXIT_FAILURE );
    }
}

#define HANDLE_ERROR( err ) (HandleError( err, __FILE__, __LINE__ ))


__global__ void dotProd(int length, float *u, float *v, float *out) {
    unsigned tid = threadIdx.x + blockDim.x * blockIdx.x;
    unsigned tid_const = threadIdx.x + blockDim.x * blockIdx.x;
    float temp = 0;

    while (tid < length) {
        temp += u[tid] * v[tid];
        tid  += blockDim.x * gridDim.x;
    }
    out[tid_const] = temp;
}


__global__ void dotProdWithSharedMem(int length, float *u, float *v, float *out) {
    __shared__ float cache[nThreadsPerBlock];
    unsigned tid = threadIdx.x + blockDim.x * blockIdx.x;
    unsigned cid = threadIdx.x;

    float temp = 0;
    while (tid < length) {
        temp += u[tid] * v[tid];
        tid  += blockDim.x * gridDim.x;
    }

    cache[cid] = temp;
    __syncthreads();

    int i = blockDim.x/2;
    while (i != 0) {
        if (cid < i) {
            cache[cid] += cache[cid + i];
        }
        __syncthreads();
        i /= 2;
    }

    if (cid == 0) {
        out[blockIdx.x] = cache[0];
    }
}


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

    size_t vec_len  = 1 << std::stoi(argv[1]);
    size_t size     = vec_len * sizeof(float);
    size_t nblocks  = std::stoi(argv[2]);
    size_t size_out   = nThreadsPerBlock*nblocks*sizeof(float);
    size_t size_out_2 = nblocks*sizeof(float);

    float *u     = (float *)malloc(size);
    float *v     = (float *)malloc(size);
    float *out   = (float *)malloc(size_out);
    float *out_2 = (float *)malloc(size_out_2);

    float *dev_u, *dev_v, *dev_out, *dev_out_2; // Device arrays

    float res_gpu = 0;
    float res_gpu_2 = 0;
    float res_cpu = 0;

    dim3 dimGrid(nblocks, 1, 1);
    dim3 dimBlocks(nThreadsPerBlock, 1, 1);

    // Initiate values
    for(size_t i=0; i<vec_len; ++i) {
        u[i] = std::sin(i*PI*1E-2);
        v[i] = std::cos(i*PI*1E-2);
    }

    HANDLE_ERROR( cudaMalloc((void**)&dev_u, size) );
    HANDLE_ERROR( cudaMalloc((void**)&dev_v, size) );
    HANDLE_ERROR( cudaMalloc((void**)&dev_out, size_out) );
    HANDLE_ERROR( cudaMalloc((void**)&dev_out_2, size_out_2) );
    HANDLE_ERROR( cudaMemcpy(dev_u, u, size, cudaMemcpyHostToDevice) );
    HANDLE_ERROR( cudaMemcpy(dev_v, v, size, cudaMemcpyHostToDevice) );


    auto t1_gpu = std::chrono::system_clock::now();
    dotProd <<<dimGrid, dimBlocks>>> (vec_len, dev_u, dev_v, dev_out);
    cudaDeviceSynchronize();
    HANDLE_ERROR( cudaMemcpy(out, dev_out, size_out, cudaMemcpyDeviceToHost) );
    // Reduction
    for(size_t i=0; i<nThreadsPerBlock*nblocks; ++i) {
        res_gpu += out[i];
    }


    auto t2_gpu = std::chrono::system_clock::now();
    // GPU version with shared memory
    dotProdWithSharedMem <<<dimGrid, dimBlocks>>> (vec_len, dev_u, dev_v, dev_out_2);
    cudaDeviceSynchronize();
    HANDLE_ERROR( cudaMemcpy(out_2, dev_out_2, size_out_2, cudaMemcpyDeviceToHost) );
    // Reduction
    for(size_t i=0; i<nblocks; ++i) {
        res_gpu_2 += out_2[i];
    }
    auto t3_gpu = std::chrono::system_clock::now();


    // CPU version for result-check
    for(size_t i=0; i<vec_len; ++i) {
        res_cpu += u[i] * v[i];
    }
    auto t2_cpu = std::chrono::system_clock::now();


    double t_gpu = std::chrono::duration <double, std::milli> (t2_gpu - t1_gpu).count();
    double t_gpu_2 = std::chrono::duration <double, std::milli> (t3_gpu - t2_gpu).count();
    double t_cpu = std::chrono::duration <double, std::milli> (t2_cpu - t3_gpu).count();

    printf("Number of threads per block : %i \n", nThreadsPerBlock);
    printf("Number of blocks in the grid: %i \n", nblocks);
    printf("Total number of threads     : %i \n", nThreadsPerBlock*nblocks);
    printf("Length of vectors           : %i \n\n", vec_len);
    printf("GPU using registers: %.10f, time consummed: %.5f ms\n", res_gpu, t_gpu);
    printf("GPU using shared   : %.10f, time consummed: %.5f ms\n", res_gpu_2, t_gpu_2);
    printf("CPU result         : %.10f, time consummed: %.5f ms\n", res_cpu, t_cpu);

    cudaFree(dev_u);
    cudaFree(dev_v);
    cudaFree(dev_out);
    cudaFree(dev_out_2);
    free(u);
    free(v);
    free(out);
    free(out_2);

    return 0;
}

感谢您耐心看完这篇长文!任何帮助将不胜感激!

尼可

【问题讨论】:

    标签: cuda


    【解决方案1】:

    您正在探索float 精度的限制以及与浮点运算顺序相关的变化。这里的实际“准确性”将取决于确切的数据和确切的操作顺序。不同的算法会有不同的运算顺序,因此会有不同的结果。

    您可能想阅读this paper

    您似乎做出的假设之一是 CPU 结果是准确的,没有任何理由证明该假设

    如果我们将“准确度”定义为结果与数值正确结果之间的差异(即“接近度”),我怀疑共享内存结果更准确。

    如果我们将您的代码转换为使用 double 类型而不是 float 类型,我们观察到:

    1. 所有 3 种方法的结果更接近(在打印输出中相同)。
    2. double 结果与任何float 大小写都不匹配。
    3. float 案例的共享内存结果实际上是最接近 double 案例结果的结果。

    这是一个证明这一点的测试用例:

    $ cat t397.cu
    #include <iostream>
    #include <string>
    #include <cmath>
    #include <chrono>
    #include <cuda.h>
    
    #ifndef USE_DOUBLE
    typedef float ft;
    #else
    typedef double ft;
    #endif
    #define PI (ft) 3.141592653589793
    
    const size_t nThreadsPerBlock = 256;
    
    
    static void HandleError(cudaError_t err, const char *file, int line )
    {
        if (err != cudaSuccess) {
        printf( "%s in %s at line %d\n", cudaGetErrorString( err ),
                file, line );
        exit( EXIT_FAILURE );
        }
    }
    
    #define HANDLE_ERROR( err ) (HandleError( err, __FILE__, __LINE__ ))
    
    
    __global__ void dotProd(int length, ft *u, ft *v, ft *out) {
        unsigned tid = threadIdx.x + blockDim.x * blockIdx.x;
        unsigned tid_const = threadIdx.x + blockDim.x * blockIdx.x;
        ft temp = 0;
    
        while (tid < length) {
            temp += u[tid] * v[tid];
            tid  += blockDim.x * gridDim.x;
        }
        out[tid_const] = temp;
    }
    
    
    __global__ void dotProdWithSharedMem(int length, ft *u, ft *v, ft *out) {
        __shared__ ft cache[nThreadsPerBlock];
        unsigned tid = threadIdx.x + blockDim.x * blockIdx.x;
        unsigned cid = threadIdx.x;
    
        ft temp = 0;
        while (tid < length) {
            temp += u[tid] * v[tid];
            tid  += blockDim.x * gridDim.x;
        }
    
        cache[cid] = temp;
        __syncthreads();
    
        int i = blockDim.x/2;
        while (i != 0) {
            if (cid < i) {
                cache[cid] += cache[cid + i];
            }
            __syncthreads();
            i /= 2;
        }
    
        if (cid == 0) {
            out[blockIdx.x] = cache[0];
        }
    }
    
    
    int main(int argc, char* argv[]) {
    
        size_t vec_len  = 1 << std::stoi(argv[1]);
        size_t size     = vec_len * sizeof(ft);
        size_t nblocks  = std::stoi(argv[2]);
        size_t size_out   = nThreadsPerBlock*nblocks*sizeof(ft);
        size_t size_out_2 = nblocks*sizeof(ft);
    
        ft *u     = (ft *)malloc(size);
        ft *v     = (ft *)malloc(size);
        ft *out   = (ft *)malloc(size_out);
        ft *out_2 = (ft *)malloc(size_out_2);
    
        ft *dev_u, *dev_v, *dev_out, *dev_out_2; // Device arrays
    
        ft res_gpu = 0;
        ft res_gpu_2 = 0;
        ft res_cpu = 0;
    
        dim3 dimGrid(nblocks, 1, 1);
        dim3 dimBlocks(nThreadsPerBlock, 1, 1);
    
        // Initiate values
        for(size_t i=0; i<vec_len; ++i) {
            u[i] = std::sin(i*PI*1E-2);
            v[i] = std::cos(i*PI*1E-2);
        }
    
        HANDLE_ERROR( cudaMalloc((void**)&dev_u, size) );
        HANDLE_ERROR( cudaMalloc((void**)&dev_v, size) );
        HANDLE_ERROR( cudaMalloc((void**)&dev_out, size_out) );
        HANDLE_ERROR( cudaMalloc((void**)&dev_out_2, size_out_2) );
        HANDLE_ERROR( cudaMemcpy(dev_u, u, size, cudaMemcpyHostToDevice) );
        HANDLE_ERROR( cudaMemcpy(dev_v, v, size, cudaMemcpyHostToDevice) );
    
    
        auto t1_gpu = std::chrono::system_clock::now();
        dotProd <<<dimGrid, dimBlocks>>> (vec_len, dev_u, dev_v, dev_out);
        cudaDeviceSynchronize();
        HANDLE_ERROR( cudaMemcpy(out, dev_out, size_out, cudaMemcpyDeviceToHost) );
        // Reduction
        for(size_t i=0; i<nThreadsPerBlock*nblocks; ++i) {
            res_gpu += out[i];
        }
    
    
        auto t2_gpu = std::chrono::system_clock::now();
        // GPU version with shared memory
        dotProdWithSharedMem <<<dimGrid, dimBlocks>>> (vec_len, dev_u, dev_v, dev_out_2);
        cudaDeviceSynchronize();
        HANDLE_ERROR( cudaMemcpy(out_2, dev_out_2, size_out_2, cudaMemcpyDeviceToHost) );
        // Reduction
        for(size_t i=0; i<nblocks; ++i) {
            res_gpu_2 += out_2[i];
        }
        auto t3_gpu = std::chrono::system_clock::now();
    
    
        // CPU version for result-check
        for(size_t i=0; i<vec_len; ++i) {
            res_cpu += u[i] * v[i];
        }
        auto t2_cpu = std::chrono::system_clock::now();
    
    
        double t_gpu = std::chrono::duration <double, std::milli> (t2_gpu - t1_gpu).count();
        double t_gpu_2 = std::chrono::duration <double, std::milli> (t3_gpu - t2_gpu).count();
        double t_cpu = std::chrono::duration <double, std::milli> (t2_cpu - t3_gpu).count();
    
        printf("Number of threads per block : %i \n", nThreadsPerBlock);
        printf("Number of blocks in the grid: %i \n", nblocks);
        printf("Total number of threads     : %i \n", nThreadsPerBlock*nblocks);
        printf("Length of vectors           : %i \n\n", vec_len);
        printf("GPU using registers: %.10f, time consummed: %.5f ms\n", res_gpu, t_gpu);
        printf("GPU using shared   : %.10f, time consummed: %.5f ms\n", res_gpu_2, t_gpu_2);
        printf("CPU result         : %.10f, time consummed: %.5f ms\n", res_cpu, t_cpu);
    
        cudaFree(dev_u);
        cudaFree(dev_v);
        cudaFree(dev_out);
        cudaFree(dev_out_2);
        free(u);
        free(v);
        free(out);
        free(out_2);
    
        return 0;
    }
    $ nvcc -std=c++11 t397.cu -o t397
    $ ./t397 17 512
    Number of threads per block : 256
    Number of blocks in the grid: 512
    Total number of threads     : 131072
    Length of vectors           : 131072
    
    GPU using registers: 9.6904191971, time consummed: 0.89290 ms
    GPU using shared   : 9.6906833649, time consummed: 0.04289 ms
    CPU result         : 9.6904191971, time consummed: 0.41527 ms
    $ nvcc -std=c++11 t397.cu -o t397 -DUSE_DOUBLE
    $ ./t397 17 512
    Number of threads per block : 256
    Number of blocks in the grid: 512
    Total number of threads     : 131072
    Length of vectors           : 131072
    
    GPU using registers: 9.6913433287, time consummed: 1.33016 ms
    GPU using shared   : 9.6913433287, time consummed: 0.05032 ms
    CPU result         : 9.6913433287, time consummed: 0.41275 ms
    $
    

    【讨论】:

    • 非常感谢罗伯特的出色回答。确实,对于三个结果的正确性,我没有做足够的验证和验证。顺便说一句,我正要问你关于我的第三个问题,但突然注意到共享内存中的对象只存在于块中。它们无法从其他块中的线程接收值。你提到的文章很有帮助。祝你有美好的一天:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-04-09
    • 1970-01-01
    • 2012-09-30
    • 1970-01-01
    • 1970-01-01
    • 2023-04-10
    • 1970-01-01
    相关资源
    最近更新 更多