使用Nvidia的Thrust库标准化一堆向量

时间:2011-04-20 14:18:30

标签: c++ vector cuda gpu thrust

我刚刚了解了Nvidia的推力库。只是为了尝试它写了一个小例子,它应该规范化一堆矢量。

#include <cstdio>

#include <thrust/transform.h>
#include <thrust/device_vector.h>
#include <thrust/host_vector.h>

struct normalize_functor: public thrust::unary_function<double4, double4>
{
    __device__ __host__ double4 operator()(double4 v)
    {
        double len = sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
        v.x /= len;
        v.y /= len;
        v.z /= len;
        printf("%f %f %f\n", v.x, v.y, v.z);
    }
};

int main()
{
    thrust::host_vector<double4> v(2);
    v[0].x = 1; v[0].y = 2; v[0].z = 3;
    v[1].x = 4; v[1].y = 5; v[1].z = 6;

    thrust::device_vector<double4> v_d = v; 
    thrust::for_each(v_d.begin(), v_d.end(), normalize_functor());

    // This doesn't seem to copy back
    v = v_d;

    // Neither this does..
    thrust::host_vector<double4> result = v_d;

    for(int i=0; i<v.size(); i++)
        printf("[ %f %f %f ]\n", result[i].x, result[i].y, result[i].z);

    return 0;
}

上面的例子似乎有效,但我无法复制数据..我认为一个简单的赋值会调用cudaMemcpy。它的工作原理是将数据从主机复制到设备但不返回???

其次我不确定我是否以正确的方式这样做。 for_each的文档说:

  

for_each将函数对象f应用于[first,last]范围内的每个元素; f的返回值(如果有)将被忽略。

但是unary_function结构模板需要两个模板参数(一个用于返回值)并强制operator()也返回一个值,这会在编译时产生警告。我不知道我应该怎么写一个没有返回值的一元仿函数。

接下来是数据安排。我只选择了double4,因为这将导致两个获取指令ld.v2.f64和ld.f64 IIRC。但是我想知道如何在引擎盖下获取数据(以及有多少cuda线程/块)。如果我选择4个向量的结构,它将能够以合并的方式获取数据。

最后推力提供了元组。一系列元组怎么样?在这种情况下如何安排数据。

我查看了这些示例,但是我没有找到一个示例来解释为一堆向量选择哪种数据结构(dot_products_with_zip.cu示例说明了“数组结构”而不是“结构数组”但我看到示例中没有使用任何结构。

更新

我修复了上面的代码并试图运行一个更大的例子,这次规范化10k向量。

#include <cstdio>

#include <thrust/transform.h>
#include <thrust/device_vector.h>
#include <thrust/host_vector.h>

struct normalize_functor
{
    __device__ __host__ void operator()(double4& v)
    {
        double len = sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
        v.x /= len;
        v.y /= len;
        v.z /= len;
    }
};

int main()
{
    int n = 10000;
    thrust::host_vector<double4> v(n);
    for(int i=0; i<n; i++) {
        v[i].x = rand();
        v[i].y = rand();
        v[i].z = rand();
    }

    thrust::device_vector<double4> v_d = v;

    thrust::for_each(v_d.begin(), v_d.end(), normalize_functor());

    v = v_d;

    return 0;
}

使用computeprof进行分析显示占用率低且未合并的内存访问:

Kernel Occupancy Analysis

Kernel details : Grid size: 23 x 1 x 1, Block size: 448 x 1 x 1
Register Ratio      = 0.984375  ( 32256 / 32768 ) [24 registers per thread] 
Shared Memory Ratio     = 0 ( 0 / 49152 ) [0 bytes per Block] 
Active Blocks per SM        = 3 / 8
Active threads per SM       = 1344 / 1536
Potential Occupancy     = 0.875  ( 42 / 48 )
Max achieved occupancy  = 0.583333  (on 9 SMs)
Min achieved occupancy  = 0.291667  (on 5 SMs)
Occupancy limiting factor   = Block-Size

Memory Throughput Analysis for kernel launch_closure_by_value on device GeForce GTX 470

Kernel requested global memory read throughput(GB/s): 29.21
Kernel requested global memory write throughput(GB/s): 17.52
Kernel requested global memory throughput(GB/s): 46.73
L1 cache read throughput(GB/s): 100.40
L1 cache global hit ratio (%): 48.15
Texture cache memory throughput(GB/s): 0.00
Texture cache hit rate(%): 0.00
L2 cache texture memory read throughput(GB/s): 0.00
L2 cache global memory read throughput(GB/s): 42.44
L2 cache global memory write throughput(GB/s): 46.73
L2 cache global memory throughput(GB/s): 89.17
L2 cache read hit ratio(%): 88.86
L2 cache write hit ratio(%): 3.09
Local memory bus traffic(%): 0.00
Global memory excess load(%): 31.18
Global memory excess store(%): 62.50
Achieved global memory read throughput(GB/s): 4.73
Achieved global memory write throughput(GB/s): 45.29
Achieved global memory throughput(GB/s): 50.01
Peak global memory throughput(GB/s): 133.92

我想知道如何优化这个?

2 个答案:

答案 0 :(得分:4)

如果您想使用for_each就地修改序列,那么您需要在仿函数中通过引用获取参数:

struct normalize_functor
{
    __device__ __host__ void operator()(double4& ref)
    {
        double v = ref;
        double len = sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
        v.x /= len;
        v.y /= len;
        v.z /= len;
        printf("%f %f %f\n", v.x, v.y, v.z);
        ref = v;
    }
};

或者,您可以将normalize_functor的定义与transform算法一起使用,将v_d指定为源和目标范围:

thrust::transform(v_d.begin(), v_d.end(), v_d.begin(), normalize_functor());

我个人的偏好是在这种情况下使用transform,但两种情况下的表现都应该相同。

答案 1 :(得分:1)

关于优化问题,Thrust没有太多可以做的事情 - 这不是图书馆的意图。不想为作为Thrust的作者之一的Nathan Bell发言,并且已经在这个帖子中发帖,目的是以简单,直观的方式为GPU提供一系列数据并行算法,而无需写太多,如果有的话,CUDA代码。在我看来,它在这方面取得了巨大的成功。许多推力内核的内核性能接近现有技术水平,但总有一些优化可以在特定情况下完成,这在通用模板代码中并不容易。这是您为易用性和灵活性所付出的代价的一部分。

话虽如此,我怀疑在你的操作员功能中有一些调整可能会改善一些事情。我通常会写这样的东西:

struct normalize_functor
{
    __device__ __host__ void operator()(double4& v)
    {
        double4 nv = v;
        double len = sqrt(nv.x*nv.x + nv.y*nv.y + nv.z*nv.z);
        nv.x /= len;
        nv.y /= len;
        nv.z /= len;
        (void)nv.h;
        v = nv;
    };
};

现在它虽然没有原版那么漂亮,但它应该确保编译器发出矢量化加载和存储指令。我已经看到过去编译器将优化载荷和存储未使用的向量类型成员的情况,这导致PTX生成器发出标量加载和存储,并因此中断合并。通过清除float4加载和存储,并确保使用结构的每个元素,它可以绕过至少在2.x和3.x nvcc版本中出现的这种不需要的“优化”。我不确定4.0编译器是否仍然如此。