关于gpu的模运算

时间:2012-09-03 18:57:19

标签: cuda gpgpu

我正在研究GPU算法,该算法应该进行大量的模块化计算。特别是,对于有限域中的矩阵的各种操作,从长远来看 简化为原始操作,如:(a * b - c * d)mod m或(a * b + c)mod m其中a,b,c和d是模m的残差,m是32位素数。

通过实验,我了解到算法的性能主要受到慢模运算的限制,因为硬件上GPU不支持整数模(%)和除法运算。

如果有人能让我了解如何使用CUDA实现高效的模块化计算,我很感激。

要了解如何在CUDA上实现这一点,我使用以下代码片段:

__global__ void mod_kernel(unsigned *gout, const unsigned *gin) {

unsigned tid = threadIdx.x;
unsigned a = gin[tid], b = gin[tid * 2], m = gin[tid * 3];

typedef unsigned long long u64;

__syncthreads();
unsigned r = (unsigned)(((u64)a * (u64)b) % m);
__syncthreads();
gout[tid] = r;
}

这段代码不应该起作用,我只想看看模块化的减少方式 在CUDA上实施。

当我用cuobjdump --dump-sass对此进行反汇编时(感谢njuffa的建议!),我看到以下内容:

/*0098*/     /*0xffffdc0450ee0000*/     BAR.RED.POPC RZ, RZ;
/*00a0*/     /*0x1c315c4350000000*/     IMUL.U32.U32.HI R5, R3, R7;
/*00a8*/     /*0x1c311c0350000000*/     IMUL.U32.U32 R4, R3, R7;
/*00b0*/     /*0xfc01dde428000000*/     MOV R7, RZ;
/*00b8*/     /*0xe001000750000000*/     CAL 0xf8;
/*00c0*/     /*0x00000007d0000000*/     BPT.DRAIN 0x0;
/*00c8*/     /*0xffffdc0450ee0000*/     BAR.RED.POPC RZ, RZ;

请注意,在对bar.red.popc的两次调用之间,会调用0xf8过程,该过程实现了一些复杂的算法(大约50条指令甚至更多)。不要轻视mod(%)操作很慢

3 个答案:

答案 0 :(得分:13)

前段时间我在GPU上进行了模块化算法的实验。在Fermi GPU上,您可以使用双精度算法来避免昂贵的div和mod操作。例如,模块化乘法可以如下完成:

// fast truncation of double-precision to integers
#define CUMP_D2I_TRUNC (double)(3ll << 51)
// computes r = a + b subop c unsigned using extended precision
#define VADDx(r, a, b, c, subop) \
    asm volatile("vadd.u32.u32.u32." subop " %0, %1, %2, %3;" :  \
            "=r"(r) : "r"(a) , "r"(b), "r"(c));

// computes a * b mod m; invk = (double)(1<<30) / m
__device__ __forceinline__ 
unsigned mul_m(unsigned a, unsigned b, volatile unsigned m,
    volatile double invk) { 

   unsigned hi = __umulhi(a*2, b*2); // 3 flops
   // 2 double instructions
   double rf = __uint2double_rn(hi) * invk + CUMP_D2I_TRUNC;
   unsigned r = (unsigned)__double2loint(rf);
   r = a * b - r * m; // 2 flops

   // can also be replaced by: VADDx(r, r, m, r, "min") // == umin(r, r + m);
   if((int)r < 0) 
      r += m;
   return r;
}

但是这只适用于31位整数模(如果1位对你来说并不重要) 你还需要事先预先计算'invk'。这给出了我可以达到的绝对最小指令,即:

SHL.W R2, R4, 0x1;
SHL.W R8, R6, 0x1;
IMUL.U32.U32 R4, R4, R6;
IMUL.U32.U32.HI R8, R2, R8;
I2F.F64.U32 R8, R8;
DFMA R2, R2, R8, R10;
IMAD.U32.U32 R4, -R12, R2, R4;
ISETP.GE.AND P0, pt, R4, RZ, pt;
@!P0 IADD R4, R12, R4;

有关算法的说明,您可以查看我的论文: gpu_resultants。其他操作如(x y - z w)mod m也在那里解释。

出于好奇,我比较了算法的性能 使用模块化乘法:

unsigned r = (unsigned)(((u64)a * (u64)b) % m);

使用mul_m对抗优化版本。

使用默认%操作的模块化算术:

low_deg: 11; high_deg: 2481; bits: 10227
nmods: 330; n_real_pts: 2482; npts: 2495

res time: 5755.357910 ms; mod_inv time: 0.907008 ms; interp time: 856.015015 ms; CRA time: 44.065857 ms
GPU time elapsed: 6659.405273 ms; 

使用mul_m进行模块化算术:

low_deg: 11; high_deg: 2481; bits: 10227
nmods: 330; n_real_pts: 2482; npts: 2495

res time: 1100.124756 ms; mod_inv time: 0.192608 ms; interp time: 220.615143 ms; CRA time: 10.376352 ms
GPU time elapsed: 1334.742310 ms; 

所以平均来说它快5倍左右。另请注意,如果您只是使用具有一堆mul_mod操作的内核(例如<​​em> saxpy 示例)来评估 raw 算术性能,则可能看不到加速。但在具有控制逻辑,同步障碍等的实际应用中,加速非常明显。

答案 1 :(得分:9)

高端Fermi GPU(例如GTX 580)可能会为您提供运输卡中的最佳性能。您希望所有32位操作数都是“unsigned int”类型以获得最佳性能,因为处理有符号的除法和模数会有一些额外的开销。

编译器使用固定除数生成非常有效的除法和模的代码我记得在Fermi和Kepler上通常有大约三到五个机器指令指令。您可以使用cuobjdump --dump-sass检查生成的SASS(机器代码)。如果只使用几个不同的除数,则可以使用具有常数除数的模板化函数。

您应该看到在Fermi和Kepler上为带有可变除数的无符号32位运算生成的16个内联SASS指令的顺序。代码受到整数乘法吞吐量的限制,而Fermi级GPU与硬件解决方案相比具有竞争力。由于整数倍吞吐量降低,目前出货的Kepler级GPU的性能有所降低。

[稍后补充,澄清问题后:]

另一方面,无符号64位除法和带有可变除数的模数称为Fermi和Kepler上约65条指令的子程序。它们看起来接近最佳状态。在Fermi上,这仍然与硬件实现相当具有竞争力(请注意,在提供此内置指令的CPU上,64位整数除法并不是非常快。下面是我在一段时间内发布到NVIDIA论坛的一些代码,用于澄清中描述的任务类型。它避免了昂贵的划分,但确实假设相当大批量的操作数共享相同的分配。它使用双精度算法,特别是在特斯拉级GPU上(与消费卡相对)。我只对该代码进行了粗略的测试,您可能希望在部署之前更仔细地测试它。

// Let b, p, and A[i] be integers < 2^51
// Let N be a integer on the order of 10000
// for i from 1 to N
// A[i] <-- A[i] * b mod p

/*---- kernel arguments ----*/
unsigned long long *A;
double b, p; /* convert from unsigned long long to double before passing to kernel */
double oop;  /* pass precomputed 1.0/p to kernel */

/*---- code inside kernel -----*/
double a, q, h, l, rem;
const double int_cvt_magic = 6755399441055744.0; /* 2^52+2^51 */

a = (double)A[i];

/* approximate quotient and round it to the nearest integer */
q = __fma_rn (a * b, oop, int_cvt_magic);
q = q - int_cvt_magic;

/* back-multiply, representing p*q as a double-double h:l exactly */
h = p * q;
l = __fma_rn (p, q, -h);

/* remainder is double-width product a*b minus double-double h:l */
rem = __fma_rn (a, b, -h);
rem = rem - l;

/* remainder may be negative as quotient rounded; fix if necessary */
if (rem < 0.0) rem += p;

A[i] = (unsigned long long)rem;

答案 2 :(得分:1)

有效地执行mod操作有一些技巧,但如果只有m是基数2。

例如,x mod y == x&amp; (y-1),其中y是2 ^ n。执行按位操作是最快的。

否则,可能是一张查询表? 以下是关于有效模数实现的讨论的链接。您可能需要自己实现它以充分利用它。

Efficient computation of mod