我发现Thrust可以提供的内容非常有限,如下面的代码所示: 我最终有9 * 9 * 2(1多个+ 1减少)推力调用,这是162内核启动。 如果我编写自己的内核,只需要启动1个内核。
for(i=1;i<=9;i++)
{
for(j=i;j<=9;j++)
{
ATA[i][j]=0;
for(m=1;m<=50000;m++)
ATA[i][j]=ATA[i][j]+X[idx0[i]][m]*X[idx0[j]][m];
}
}
然后我最终得到了Thrust实施:
for(i=1;i<=dim0;i++)
{
for(j=i;j<=dim0;j++)
{
thrust::transform(t_d_X+(idx0[i]-1)*(1+iNumPaths)+1, t_d_X+(idx0[i]-1)*(1+iNumPaths)+iNumPaths+1, t_d_X+(idx0[j]-1)*(1+iNumPaths)+1,t_d_cdataMulti, thrust::multiplies<double>());
ATA[i][j] = thrust::reduce(t_d_cdataMulti, t_d_cdataMulti+iNumPaths, (double) 0, thrust::plus<double>());
}
}
一些分析:
transform_reduce
:没有帮助,因为有一个指针重定向idx0[i]
,基本上涉及2个数组。第一个是X[idx0[i]]
,第二个是X[idx0[j]]
reduce_by_key
:会有所帮助。但我需要将所有中间结果存储到一个大数组中,并准备一个大小相同的巨大映射关键表。将试一试。
transform_iterator
:没有帮助,原因与1相同。
认为我无法避免编写自己的内核?
答案 0 :(得分:2)
我打赌@ m.s。可以提供更有效的方法。但这是一种可能的方法。为了通过推力将整个计算简化为单个内核调用,有必要使用单个推力算法调用来处理所有内容。在操作的核心,我们将许多计算汇总在一起,以填充矩阵。因此,我认为thrust::reduce_by_key是一种合适的推力算法。这意味着我们必须使用各种推力&#34;花式迭代器&#34;来实现所有其他转换,这些转换大多包含在thrust getting started guide中。
尝试执行此操作(使用单个内核调用处理所有内容)会使代码非常密集且难以阅读。我通常不喜欢以这种方式展示推力,但由于这是你问题的症结所在,所以无法避免。因此,让我们将reduce_by_key调用中包含的操作序列大约从内向外解包。该算法的一般基础是“平坦化”#34;将所有数据转换为单个长逻辑向量。让我们假设我们的方形矩阵尺寸仅为2x2,m
向量的长度为3.您可以想到&#34;展平&#34;或者像这样的线性索引转换:
linear index: 0 1 2 3 4 5 6 7 8 9 10 11
i index: 0 0 0 0 0 0 1 1 1 1 1 1
j index: 0 0 0 1 1 1 0 0 0 1 1 1
m index: 0 1 2 0 1 2 0 1 2 0 1 2
k index: 0 0 0 1 1 1 2 2 2 3 3 3
&#34; k指数&#34;上面是我们的密钥,最终将由reduce_by_key用于为矩阵的每个元素一起收集产品术语。请注意,代码包含EXT_I
,EXT_J
,EXT_M
和EXT_K
辅助宏,它们将使用thrust placeholders定义要对线性执行的操作index(使用counting_iterator创建)以生成各种其他&#34;索引&#34;。
我们需要做的第一件事就是构建一个合适的推力操作,将线性指数转换为idx0[i]
的转换值(再次,从&#34;向内到外向&#34; )。我们可以使用idx0
向量上的置换迭代器执行此操作,并使用transform_iterator提供&#34; map&#34; for permutation iterator - 这个转换迭代器只是将线性索引(mb
)转换为&#34; i&#34;指数:
thrust::make_permutation_iterator(d_idx0.begin(), thrust::make_transform_iterator(mb, EXT_I))
现在我们需要将步骤1的结果与其他索引(在这种情况下为m
)结合起来,生成二维索引的线性化版本为X
({{1} }是d_X
)的向量线性化版本。为此,我们将把zip_iterator中第一步的结果与另一个创建X
索引的转换迭代器结合起来。这个zip_iterator将传递给一个transform_iterator,它接受两个索引并将其转换为线性化索引,以便&#34;查看&#34; m
向量:
d_X
thrust::make_transform_iterator(thrust::make_zip_iterator(thrust::make_tuple(thrust::make_permutation_iterator(d_idx0.begin(), thrust::make_transform_iterator(mb, EXT_I)), thrust::make_transform_iterator(mb, EXT_M))), create_Xidx()))
是将两个计算索引转换为线性索引并将其转换为create_Xidx
使用步骤2的结果,我们可以使用置换迭代器从乘法中的第一项中获取d_X
的适当值:
d_X
使用thrust::make_permutation_iterator(d_X.begin(), {code from step 2})
代替EXT_J
重复步骤1,2,3,在乘法中创建第二个词:
EXT_I
将在步骤3和4中创建的术语放入zip_iterator中,供transform_iterator使用,它将两者相乘(使用X[idx0[i]][m]*X[idx0[j]][m]
仿函数)来创建实际产品:
my_mult
reduce_by_key的其余部分相当简单。我们如前所述创建密钥索引,然后使用它将方形矩阵的每个元素的各种产品加在一起。
这是一个完整的例子:
thrust::make_transform_iterator(thrust::make_zip_iterator(thrust::make_tuple({result from step 3}, {result from step 4}, my_mult())
注意:
如果您使用以下代码对上述代码进行了分析: $ cat t875.cu
#include <iostream>
#include <thrust/reduce.h>
#include <thrust/copy.h>
#include <thrust/device_vector.h>
#include <thrust/host_vector.h>
#include <thrust/iterator/permutation_iterator.h>
#include <thrust/iterator/transform_iterator.h>
#include <thrust/iterator/zip_iterator.h>
#include <thrust/iterator/counting_iterator.h>
#include <thrust/iterator/discard_iterator.h>
// rows
#define D1 9
// cols
#define D2 9
// size of m
#define D3 50
// helpers to convert linear indices to i,j,m or "key" indices
#define EXT_I (_1/(D2*D3))
#define EXT_J ((_1/(D3))%D2)
#define EXT_M (_1%D3)
#define EXT_K (_1/D3)
void test_cpu(float ATA[][D2], float X[][D3], int idx0[]){
for(int i=0;i<D1;i++)
{
for(int j=0;j<D2;j++)
{
ATA[i][j]=0;
for(int m=0;m<D3;m++)
ATA[i][j]=ATA[i][j]+X[idx0[i]][m]*X[idx0[j]][m];
}
}
}
using namespace thrust::placeholders;
struct create_Xidx : public thrust::unary_function<thrust::tuple<int, int>, int>{
__host__ __device__
int operator()(thrust::tuple<int, int> &my_tuple){
return (thrust::get<0>(my_tuple) * D3) + thrust::get<1>(my_tuple);
}
};
struct my_mult : public thrust::unary_function<thrust::tuple<float, float>, float>{
__host__ __device__
float operator()(thrust::tuple<float, float> &my_tuple){
return thrust::get<0>(my_tuple) * thrust::get<1>(my_tuple);
}
};
int main(){
//synthesize data
float ATA[D1][D2];
float X[D1][D3];
int idx0[D1];
thrust::host_vector<float> h_X(D1*D3);
thrust::host_vector<int> h_idx0(D1);
for (int i = 0; i < D1; i++){
idx0[i] = (i + 2)%D1; h_idx0[i] = idx0[i];
for (int j = 0; j < D2; j++) {ATA[i][j] = 0;}
for (int j = 0; j < D3; j++) {X[i][j] = j%(i+1); h_X[i*D3+j] = X[i][j];}}
thrust::device_vector<float> d_ATA(D1*D2);
thrust::device_vector<float> d_X = h_X;
thrust::device_vector<int> d_idx0 = h_idx0;
// helpers
thrust::counting_iterator<int> mb = thrust::make_counting_iterator(0);
thrust::counting_iterator<int> me = thrust::make_counting_iterator(D1*D2*D3);
// perform computation
thrust::reduce_by_key(thrust::make_transform_iterator(mb, EXT_K), thrust::make_transform_iterator(me, EXT_K), thrust::make_transform_iterator(thrust::make_zip_iterator(thrust::make_tuple(thrust::make_permutation_iterator(d_X.begin(), thrust::make_transform_iterator(thrust::make_zip_iterator(thrust::make_tuple(thrust::make_permutation_iterator(d_idx0.begin(), thrust::make_transform_iterator(mb, EXT_I)), thrust::make_transform_iterator(mb, EXT_M))), create_Xidx())), thrust::make_permutation_iterator(d_X.begin(), thrust::make_transform_iterator(thrust::make_zip_iterator(thrust::make_tuple(thrust::make_permutation_iterator(d_idx0.begin(), thrust::make_transform_iterator(mb, EXT_J)), thrust::make_transform_iterator(mb, EXT_M))), create_Xidx())))), my_mult()), thrust::make_discard_iterator(), d_ATA.begin());
thrust::host_vector<float> h_ATA = d_ATA;
test_cpu(ATA, X, idx0);
std::cout << "GPU: CPU: " << std::endl;
for (int i = 0; i < D1*D2; i++)
std::cout << i/D1 << "," << i%D2 << ":" << h_ATA[i] << " " << ATA[i/D1][i%D2] << std::endl;
}
$ nvcc -o t875 t875.cu
$ ./t875
GPU: CPU:
0,0:81 81
0,1:73 73
0,2:99 99
0,3:153 153
0,4:145 145
0,5:169 169
0,6:219 219
0,7:0 0
0,8:25 25
1,0:73 73
1,1:169 169
1,2:146 146
1,3:193 193
1,4:212 212
1,5:313 313
1,6:280 280
1,7:0 0
1,8:49 49
2,0:99 99
2,1:146 146
2,2:300 300
2,3:234 234
2,4:289 289
2,5:334 334
2,6:390 390
2,7:0 0
2,8:50 50
3,0:153 153
3,1:193 193
3,2:234 234
3,3:441 441
3,4:370 370
3,5:433 433
3,6:480 480
3,7:0 0
3,8:73 73
4,0:145 145
4,1:212 212
4,2:289 289
4,3:370 370
4,4:637 637
4,5:476 476
4,6:547 547
4,7:0 0
4,8:72 72
5,0:169 169
5,1:313 313
5,2:334 334
5,3:433 433
5,4:476 476
5,5:841 841
5,6:604 604
5,7:0 0
5,8:97 97
6,0:219 219
6,1:280 280
6,2:390 390
6,3:480 480
6,4:547 547
6,5:604 604
6,6:1050 1050
6,7:0 0
6,8:94 94
7,0:0 0
7,1:0 0
7,2:0 0
7,3:0 0
7,4:0 0
7,5:0 0
7,6:0 0
7,7:0 0
7,8:0 0
8,0:25 25
8,1:49 49
8,2:50 50
8,3:73 73
8,4:72 72
8,5:97 97
8,6:94 94
8,7:0 0
8,8:25 25
$
,您将看到两个内核调用。第一个与nvprof --print-gpu-trace ./t875
创建相关联。第二个内核调用处理整个device_vector
操作。
我不知道这一切是否比你的CUDA内核更慢或更快,因为你还没有提供它。有时候,专业编写的CUDA内核可能比推进算法执行相同的操作更快。
很有可能我在这里所拥有的并不是精确您想到的算法。例如,您的代码建议您仅填写reduce_by_key
的三角形部分。但是您的描述(9 * 9 * 2)表明您希望填充ATA
中的每个位置。尽管如此,我的目的不是为了给你一个黑盒子,而是为了演示如何使用各种推力方法在单个内核调用中实现你想要的任何东西。