14

我正在研究应该进行大量模块化计算的 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 (%) 操作很慢

4

3 回答 3

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 操作的内核(如saxpy示例)来评估原始算术性能,您可能看不到加速。但在具有控制逻辑、同步屏障等的实际应用中,加速非常明显。

于 2012-09-04T12:40:59.717 回答
9

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

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

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

[在澄清问题后添加:]

另一方面,无符号 64 位除法和带变量除数的模被称为 Fermi 和 Kepler 上大约 65 条指令的子程序。它们看起来接近最佳状态。在 Fermi 上,这仍然与硬件实现相当有竞争力(请注意,64 位整数除法在提供此作为内置指令的 CPU 上并不是非常快)。下面是我在 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;
于 2012-09-04T01:13:00.747 回答
1

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

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

否则,可能是一个查找表?下面是关于高效模实现讨论的链接。您可能需要自己实现它才能充分利用它。

mod的高效计算

于 2012-09-04T03:00:05.293 回答