如何在CUDA中使用64位指针编写指针追逐基准?

时间:2016-04-05 03:54:40

标签: cuda benchmarking

This research paper在GPU上运行一系列CUDA微基准测试,以获取全局内存延迟,指令吞吐量等统计信息。This link是作者编写和运行的一组微基准测试的链接在他们的GPU上。

一个名为global.cu的微基准测试提供了一个指针追逐基准测试代码,用于测量全局内存延迟。

这是运行内核的代码。

__global__ void global_latency (unsigned int ** my_array, int array_length, int iterations, int ignore_iterations, unsigned long long * duration) {

    unsigned int start_time, end_time;
    unsigned int *j = (unsigned int*)my_array; 
    volatile unsigned long long sum_time;

    sum_time = 0;
    duration[0] = 0;

    for (int k = -ignore_iterations; k < iterations; k++) {
        if (k==0) {
            sum_time = 0; // ignore some iterations: cold icache misses
        }

        start_time = clock();
        repeat256(j=*(unsigned int **)j;) // unroll macro, simply creates an unrolled loop of 256 instructions, nothing more
        end_time = clock();

        sum_time += (end_time - start_time);
    }

    ((unsigned int*)my_array)[array_length] = (unsigned int)j;
    ((unsigned int*)my_array)[array_length+1] = (unsigned int) sum_time;
    duration[0] = sum_time;
}

在32位指针的情况下执行指针追踪的代码行是:

j = *(unsigned int**)j;

这是关键线,因为剩余的代码行仅用于时间测量。

我试图在我的GPU上运行它,但我遇到了一个问题。在没有更改的情况下运行相同的微基准标记会给我一个An illegal memory access was encountered的运行时错误。

In the same link他们解释说:

  

全局内存测试使用指针追踪代码,其中指针值存储在数组中。 GT200上的指针是32位。如果指针大小改变,则需要更改全局存储器测试,例如Fermi上的64位指针。

事实证明我的GPU是Kepler架构,它有64位指针。

如何修改最初处理32位指针的指针追踪代码位,以便使用64位指针测量全局内存延迟?

修改

来自 havogt 的答案:我应该在问题中包含的一条重要信息是代码的这一部分,其中每个条目都会建立一个内存位置数组指向下一个指针的条目。

for (i = 0; i < N; i += step) {
    // Device pointers are 32-bit on GT200.
    h_a[i] = ((unsigned int)(uintptr_t)d_a) + ((i + stride) % N)*sizeof(unsigned int);
}

1 个答案:

答案 0 :(得分:4)

简介

在我解释为使代码工作所必须做的事情之前,请让我强调以下内容:您应该非常了解正在测试的硬件和微基准测试的设计。它为什么如此重要? 原始代码是为GT200设计的,它没有用于普通全局内存负载的缓存。如果您现在只修复指针问题,您将基本上测量L2延迟(在Kepler上,默认情况下不使用L1),因为原始代码使用非常小的内存,非常适合缓存。

免责声明:对我而言,这也是第一次研究这样的基准测试代码。因此,在使用下面的代码之前,请仔细检查。在转换原始代码时,我不保证我没有犯错。

简单的解决方案(基本上测量缓存延迟)

首先,您没有在问题中包含代码的所有相关部分。最重要的部分是

for (i = 0; i < N; i += step) {
    // Device pointers are 32-bit on GT200.
    h_a[i] = ((unsigned int)(uintptr_t)d_a) + ((i + stride) % N)*sizeof(unsigned int);
}

其中构建了一个内存位置数组,其中每个条目指向下一个指针的条目。 现在您需要做的就是在设置代码和内核中用unsigned int替换所有unsigned long long int(用于存储32位指针)。

我不会发布代码,因为如果您不理解,我不建议运行此类代码,请参阅简介。如果你理解它,那就很简单。

我的解决方案

基本上我所做的就是根据需要使用尽可能多的内存来评估所有指针最大内存量为1GB。在这两种情况下,我将最后一个条目包装到第一个条目。请注意,根据步幅,许多数组条目可能未初始化(因为它们从未使用过)。

以下代码基本上是经过一些清理后的原始代码(但它仍然不是很干净,对不起......)以及内存的变化。我介绍了一个typedef

typedef unsigned long long int ptrsize_type;

要突出显示原始代码中unsigned int必须替换为unsigned long long int的位置。我使用了repeat1024宏(来自原始代码),它只复制了行j=*(ptrsize_type **)j; 1024次。

可以在measure_global_latency()中调整步幅。在输出中,步长以字节为单位。

我将对不同步幅的延迟的解释留给您。需要调整步幅,以免重复使用缓存!

#include <stdio.h> 
#include <stdint.h>

#include "repeat.h"

typedef unsigned long long int ptrsize_type;

__global__ void global_latency (ptrsize_type** my_array, int array_length, int iterations, unsigned long long * duration) {

    unsigned long long int start_time, end_time;
    ptrsize_type *j = (ptrsize_type*)my_array;
    volatile unsigned long long int sum_time;

    sum_time = 0;

    for (int k = 0; k < iterations; k++)
    {

        start_time = clock64();
        repeat1024(j=*(ptrsize_type **)j;)
        end_time = clock64();

        sum_time += (end_time - start_time);
    }

    ((ptrsize_type*)my_array)[array_length] = (ptrsize_type)j;
    ((ptrsize_type*)my_array)[array_length+1] = (ptrsize_type) sum_time;
    duration[0] = sum_time;
}

void parametric_measure_global(int N, int iterations, unsigned long long int maxMem, int stride)
{
    unsigned long long int maxMemToArraySize = maxMem / sizeof( ptrsize_type );
    unsigned long long int maxArraySizeNeeded = 1024*iterations*stride;
    unsigned long long int maxArraySize = (maxMemToArraySize<maxArraySizeNeeded)?(maxMemToArraySize):(maxArraySizeNeeded);

    ptrsize_type* h_a = new ptrsize_type[maxArraySize+2];
    ptrsize_type** d_a;
    cudaMalloc ((void **) &d_a, (maxArraySize+2)*sizeof(ptrsize_type));

    unsigned long long int* duration;
    cudaMalloc ((void **) &duration, sizeof(unsigned long long int));

    for ( int i = 0; true; i += stride)
    {
        ptrsize_type nextAddr = ((ptrsize_type)d_a)+(i+stride)*sizeof(ptrsize_type);
        if( i+stride < maxArraySize )
        {
            h_a[i] = nextAddr;
        }
        else
        {
            h_a[i] = (ptrsize_type)d_a; // point back to the first entry
            break;
        }
    }
    cudaMemcpy((void *)d_a, h_a, (maxArraySize+2)*sizeof(ptrsize_type), cudaMemcpyHostToDevice);

    unsigned long long int latency_sum = 0;
    int repeat = 1;
    for (int l=0; l <repeat; l++)
    {
        global_latency<<<1,1>>>(d_a, maxArraySize, iterations, duration);
        cudaThreadSynchronize ();

        cudaError_t error_id = cudaGetLastError();
        if (error_id != cudaSuccess)
        {
            printf("Error is %s\n", cudaGetErrorString(error_id));
        }

        unsigned long long int latency;
        cudaMemcpy( &latency, duration, sizeof(unsigned long long int), cudaMemcpyDeviceToHost);
        latency_sum += latency;
    }

    cudaFree(d_a);
    cudaFree(duration);

    delete[] h_a;
    printf("%f\n", (double)(latency_sum/(repeat*1024.0*iterations)) );
}

void measure_global_latency()
{
    int maxMem = 1024*1024*1024; // 1GB
    int N = 1024;
    int iterations = 1;

    for (int stride = 1; stride <= 1024; stride+=1)
    {
        printf ("  %5d, ", stride*sizeof( ptrsize_type ));
        parametric_measure_global( N, iterations, maxMem, stride );
    }
    for (int stride = 1024; stride <= 1024*1024; stride+=1024)
    {
        printf ("  %5d, ", stride*sizeof( ptrsize_type ));
        parametric_measure_global( N, iterations, maxMem, stride );
    }
}

int main()
{
    measure_global_latency();
    return 0;
}

编辑:

评论的更多细节:我没有包括对结果的解释,因为我不认为自己是这些基准的专家。 我不打算将解释作为读者的练习。

现在我的解释是:我得到了Kepler GPU的相同结果(L1不可用/禁用)。对于L2读取,低于200个周期的东西是你通过小步幅获得的。通过增加iterations变量来明确重用L2,可以提高准确性。

现在,棘手的任务是找到一个不重用L2缓存的步幅。在我的方法中,我只是盲目地尝试许多不同的(大)步伐,并希望L2不被重用。在那里,我也得到约500个周期的东西。当然,更好的方法是更多地考虑缓存的结构,并通过推理而不是通过反复试验推断出正确的步幅。这就是为什么我不想自己解释结果的主要原因。

为什么延迟会再次降低延迟&gt; 1MB?这种行为的原因是我使用了1GB的固定大小来获得最大内存使用量。使用1024指针查找(repeat1024),1MB的步幅恰好适合内存。更大的步幅将环绕并再次使用L2缓存中的数据。当前代码的主要问题是1024指针(1024 * 64位)仍然完美地适合L2缓存。 这会引入另一个陷阱:如果您将iterations的数量设置为某个&gt; 1并超过1024*iterations*stride*sizeof(ptrsize_type)的内存限制,您将再次使用L2缓存。

可能的解决方案:

  • 不应将最后一个条目包装到第一个元素,而应该实现更智能的包装到(未使用的!)位置,该位置在缓存行的大小和步幅之间。但是你需要非常小心,不要覆盖内存位置,特别是如果你多次环绕。