CUDA的nvvp报告了非理想的内存访问模式,但带宽几乎达到峰值

CUDA的nvvp报告了非理想的内存访问模式,但带宽几乎达到峰值,cuda,nvvp,Cuda,Nvvp,编辑:新的最小工作示例,用于说明问题,并根据评论中给出的建议更好地解释nvvp的结果 因此,我制作了一个最小的工作示例,如下所示: #include <cuComplex.h> #include <iostream> int const n = 512 * 100; typedef float real; template < class T > struct my_complex { T x; T y; }; __global__ voi

编辑:新的最小工作示例,用于说明问题,并根据评论中给出的建议更好地解释nvvp的结果

因此,我制作了一个最小的工作示例,如下所示:

#include <cuComplex.h>
#include <iostream>

int const n = 512 * 100;

typedef float real;

template < class T >
struct my_complex {
   T x;
   T y;
};

__global__ void set( my_complex< real > * a )
{
   my_complex< real > & d = a[ blockIdx.x * 1024 + threadIdx.x ];
   d = { 1.0f, 0.0f };
}

__global__ void duplicate_whole( my_complex< real > * a )
{
   my_complex< real > & d = a[ blockIdx.x * 1024 + threadIdx.x ];
   d = { 2.0f * d.x, 2.0f * d.y };
}

__global__ void duplicate_half( real * a )
{
   real & d = a[ blockIdx.x * 1024 + threadIdx.x ];
   d *= 2.0f;
}

int main()
{
   my_complex< real > * a;
   cudaMalloc( ( void * * ) & a, sizeof( my_complex< real > ) * n * 1024 );

   set<<< n, 1024 >>>( a );
   cudaDeviceSynchronize();
   duplicate_whole<<< n, 1024 >>>( a );
   cudaDeviceSynchronize();
   duplicate_half<<< 2 * n, 1024 >>>( reinterpret_cast< real * >( a ) );
   cudaDeviceSynchronize();

   my_complex< real > * a_h = new my_complex< real >[ n * 1024 ];
   cudaMemcpy( a_h, a, sizeof( my_complex< real > ) * n * 1024, cudaMemcpyDeviceToHost );

   std::cout << "( " << a_h[ 0 ].x << ", " << a_h[ 0 ].y << " )" << '\t' << "( " << a_h[ n * 1024 - 1 ].x << ", " << a_h[ n * 1024 - 1 ].y << " )"  << std::endl;

   return 0;
}
我同意我正在加载8字节的单词。我不明白的是为什么4字节是理想的字长。特别是,内核之间没有性能差异

我认为在某些情况下,这种全局存储访问模式可能会导致性能下降。这些是什么

为什么我没有得到一个性能打击

我希望这次编辑澄清了一些不清楚的地方

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

我将从一些内核代码开始,以举例说明我的问题,如下所示

template < class data_t >
__global__ void chirp_factors_multiply( std::complex< data_t > const * chirp_factors,
                                        std::complex< data_t > * data,
                                        int M,
                                        int row_length,
                                        int b,
                                        int i_0
                                        )
{
#ifndef CUGALE_MUL_SHUFFLE
    // Output array length:
    int plane_area = row_length * M;
    // Process element:
    int i = blockIdx.x * row_length + threadIdx.x + i_0;
    my_complex< data_t > const chirp_factor = ref_complex( chirp_factors[ i ] );
    my_complex< data_t > datum;
    my_complex< data_t > datum_new;

    for ( int i_b = 0; i_b < b; ++ i_b )
    {
        my_complex< data_t > & ref_datum = ref_complex( data[ i_b * plane_area + i ] );
        datum = ref_datum;
        datum_new.x = datum.x * chirp_factor.x - datum.y * chirp_factor.y;
        datum_new.y = datum.x * chirp_factor.y + datum.y * chirp_factor.x;
        ref_datum = datum_new;
    }
#else
    // Output array length:
    int plane_area = row_length * M;
    // Element to process:
    int i = blockIdx.x * row_length + ( threadIdx.x + i_0 ) / 2;
    my_complex< data_t > const chirp_factor = ref_complex( chirp_factors[ i ] );

    // Real and imaginary part of datum (not respectively for odd threads):
    data_t datum_a;
    data_t datum_b;

    // Even TIDs will read data in regular order, odd TIDs will read data in inverted order:
    int parity = ( threadIdx.x % 2 );
    int shuffle_dir = 1 - 2 * parity;
    int inwarp_tid = threadIdx.x % warpSize;

    for ( int i_b = 0; i_b < b; ++ i_b )
    {
        int data_idx = i_b * plane_area + i;
        datum_a = reinterpret_cast< data_t * >( data + data_idx )[ parity ];
        datum_b = __shfl_sync( 0xFFFFFFFF, datum_a, inwarp_tid + shuffle_dir, warpSize );

        // Even TIDs compute real part, odd TIDs compute imaginary part:
        reinterpret_cast< data_t * >( data + data_idx )[ parity ] = datum_a * chirp_factor.x - shuffle_dir * datum_b * chirp_factor.y;
    }
#endif // #ifndef CUGALE_MUL_SHUFFLE
}

让我们考虑DATAYT是浮点的情况,这是内存带宽受限的情况。正如上面所看到的,内核有两个版本,一个是每个线程读/写8个字节(一个完整的复数),另一个是每个线程读/写4个字节,然后洗牌结果,以便正确计算复数积

我之所以使用shuffle编写这个版本,是因为nvvp坚持认为每个线程读取8字节不是最好的主意,因为这种内存访问模式效率低下。即使在GTX 1050和GTX Titan Xp测试的两个系统中,内存带宽都非常接近理论最大值,情况也是如此

毫无疑问,我知道不可能有任何改进,事实确实如此:两个内核运行的时间几乎相同。因此,我的问题如下:

为什么nvvp报告每个线程读取8字节的效率低于读取4字节的效率?在什么情况下会出现这种情况

作为旁注,单精度对我来说更重要,但双精度在某些情况下也很有用。有趣的是,在data_t是double的情况下,两个内核版本之间的执行时间也没有差异,即使在这种情况下内核是计算绑定的,而shuffle版本执行的失败次数比原始版本多


注意:内核应用于具有行长列和M行的行长*M*b数据集b图像,chirp\u因子数组为行长*M。两个内核运行良好。如果您对此有疑问,我可以编辑该问题以显示对这两个版本的调用。

这里的问题与编译器的处理方式有关你的密码。nvvp只是尽职尽责地报告运行代码时发生的事情

如果在可执行文件上使用cuobjdump-sass工具,您将发现整个复制例程正在执行两个4字节加载和两个4字节存储。这不是最优的,部分原因是在每次加载和存储时都会有一个跨步,并在内存中存储交替的元素

原因是编译器不知道my_复杂结构的对齐方式。在防止编译器生成合法的8字节加载的情况下,您的结构是合法的。如前所述,我们可以通过通知编译器我们只打算在CUDA 8字节加载合法的对齐场景中使用struct来解决这个问题,即它是自然对齐的。对结构的修改如下所示:

template < class T >
struct  __align__(8) my_complex {
   T x;
   T y;
};
通过对代码的更改,编译器将为整个内核生成8字节的加载,您应该会看到来自探查器的不同报告。只有当您理解这种装饰的含义并愿意与编译器签订合同,确保情况属实时,才应该使用这种装饰。如果你做了一些不寻常的事情,比如不寻常的指针投掷,你可能会违反你的交易,并产生一个机器故障

您看不到性能差异的原因几乎肯定与CUDA加载/存储行为和GPU缓存有关

当您进行跨步加载时,GPU无论如何都会加载整个缓存线,即使在这种情况下,对于特定的加载操作,您只需要实际元素的一半。然而,你需要另一半的元素,不管怎样,想象的元素;它们将在下一条指令中加载,由于上一次加载,这条指令很可能命中缓存

在这种情况下,在跨步存储中,在一条指令中写入跨步元素,在下一条指令中写入替换元素,最终将使用其中一个缓存作为合并缓冲区。这不是CUDA术语中使用的典型意义上的聚合;这种合并只适用于单个指令。但是,缓存合并缓冲区行为允许它在写入或逐出已驻留的行之前,对该行累积多次写入。这大约是
相当于写回缓存行为。

没有特定顺序的一些建议。1.通过提供完整的2分,让其他人更容易帮助您。如果问题如您所说,那么演示它的代码不需要像您所展示的那样复杂。如果您声称问题是每个线程读取8字节而不是4字节,请编写一个代码来实现这一点。3.使用方言来描述nvvp的输出,即:nvvp坚持认为每个线程读取8字节不是最好的主意,因为这种内存访问模式效率低下。这没用。相反,请从中给出准确的输出/观察结果nvvp@RobertCrovella,我已尽力听从你的建议。
template < class T >
struct  __align__(8) my_complex {
   T x;
   T y;
};