11

私は GPU で暗号化アルゴリズムを開発してきましたが、現在、大きな整数の加算を実行するアルゴリズムに固執しています。大きな整数は、通常の方法で 32 ビット ワードの束として表されます。

たとえば、1 つのスレッドを使用して 2 つの 32 ビット ワードを追加できます。簡単にするために、追加する数値は同じ長さとブロックあたりのスレッド数 == 単語数であると仮定します。それで:

__global__ void add_kernel(int *C, const int *A, const int *B) {
     int x = A[threadIdx.x];
     int y = B[threadIdx.x];
     int z = x + y;
     int carry = (z < x);
     /** do carry propagation in parallel somehow ? */
     ............

     z = z + newcarry; // update the resulting words after carry propagation
     C[threadIdx.x] = z;
 }

いくつかのトリッキーな削減手順を介してキャリー伝播を行う方法があると確信していますが、それを理解できませんでした..

CUDA の推力拡張機能を見てみましたが、大きな整数パッケージはまだ実装されていないようです。おそらく、誰かが CUDA でそれを行う方法のヒントを教えてくれますか?

4

2 に答える 2

8

そうです、キャリーの伝播はプレフィックスサム計算を介して実行できますが、この操作のバイナリ関数を定義し、それが連想的であることを証明するのは少し難しいです (並列プレフィックスサムに必要です)。実際のところ、このアルゴリズムは (理論的には)キャリー先読み加算器で使用されます。

2 つの大きな整数 a[0..n-1] と b[0..n-1] があるとします。次に、(i = 0..n-1) を計算します。

s[i] = a[i] + b[i]l;
carryin[i] = (s[i] < a[i]);

2 つの関数を定義します。

generate[i] = carryin[i];
propagate[i] = (s[i] == 0xffffffff);

generate[i] == 1 はキャリーが位置 i で生成されることを意味し、propagate[i] == 1 はキャリーが位置 (i - 1) から (i + 1) に伝搬されることを意味します。目標は、結果の合計 s[0..n-1] を更新するために使用される関数 Carryout[0..n-1] を計算することです。キャリーアウトは、次のように再帰的に計算できます。

carryout[i] = generate[i] OR (propagate[i] AND carryout[i-1])
carryout[0] = 0

ここで、carryout[i] == 1 キャリーが位置 i で生成された場合、またはキャリーが以前に生成され、位置 i に伝搬された場合。最後に、結果の合計を更新します。

s[i] = s[i] + carryout[i-1];  for i = 1..n-1
carry = carryout[n-1];

これで、キャリーアウト関数が実際にバイナリ結合であることを証明するのは非常に簡単であり、したがって、並列プレフィックス合計計算が適用されます。これを CUDA に実装するには、フラグ「生成」と「伝播」の両方を 1 つの変数にマージできます。これらは相互に排他的であるためです。

cy[i] = (s[i] == -1u ? -1u : 0) | carryin[i];

言い換えると、

cy[i] = 0xffffffff  if propagate[i]
cy[i] = 1           if generate[i]
cy[u] = 0           otherwise

次に、次の式がキャリーアウト関数のプレフィックス合計を計算することを確認できます。

cy[i] = max((int)cy[i], (int)cy[k]) & cy[i];

すべての k < i に対して。以下のコード例は、2048 ワード整数の大きな加算を示しています。ここでは、512 スレッドの CUDA ブロックを使用しました。

// add & output carry flag
#define UADDO(c, a, b) \ 
     asm volatile("add.cc.u32 %0, %1, %2;" : "=r"(c) : "r"(a) , "r"(b));
// add with carry & output carry flag
#define UADDC(c, a, b) \ 
     asm volatile("addc.cc.u32 %0, %1, %2;" : "=r"(c) : "r"(a) , "r"(b));

#define WS 32

__global__ void bignum_add(unsigned *g_R, const unsigned *g_A,const unsigned *g_B) {

extern __shared__ unsigned shared[];
unsigned *r = shared; 

const unsigned N_THIDS = 512;
unsigned thid = threadIdx.x, thid_in_warp = thid & WS-1;
unsigned ofs, cf;

uint4 a = ((const uint4 *)g_A)[thid],
      b = ((const uint4 *)g_B)[thid];

UADDO(a.x, a.x, b.x) // adding 128-bit chunks with carry flag
UADDC(a.y, a.y, b.y)
UADDC(a.z, a.z, b.z)
UADDC(a.w, a.w, b.w)
UADDC(cf, 0, 0) // save carry-out

// memory consumption: 49 * N_THIDS / 64
// use "alternating" data layout for each pair of warps
volatile short *scan = (volatile short *)(r + 16 + thid_in_warp +
        49 * (thid / 64)) + ((thid / 32) & 1);

scan[-32] = -1; // put identity element
if(a.x == -1u && a.x == a.y && a.x == a.z && a.x == a.w)
    // this indicates that carry will propagate through the number
    cf = -1u;

// "Hillis-and-Steele-style" reduction 
scan[0] = cf;
cf = max((int)cf, (int)scan[-2]) & cf;
scan[0] = cf;
cf = max((int)cf, (int)scan[-4]) & cf;
scan[0] = cf;
cf = max((int)cf, (int)scan[-8]) & cf;
scan[0] = cf;
cf = max((int)cf, (int)scan[-16]) & cf;
scan[0] = cf;
cf = max((int)cf, (int)scan[-32]) & cf;
scan[0] = cf;

int *postscan = (int *)r + 16 + 49 * (N_THIDS / 64);
if(thid_in_warp == WS - 1) // scan leading carry-outs once again
    postscan[thid >> 5] = cf;

__syncthreads();

if(thid < N_THIDS / 32) {
    volatile int *t = (volatile int *)postscan + thid;
    t[-8] = -1; // load identity symbol
    cf = t[0];
    cf = max((int)cf, (int)t[-1]) & cf;
    t[0] = cf;
    cf = max((int)cf, (int)t[-2]) & cf;
    t[0] = cf;
    cf = max((int)cf, (int)t[-4]) & cf;
    t[0] = cf;
}
__syncthreads();

cf = scan[0];
int ps = postscan[(int)((thid >> 5) - 1)]; // postscan[-1] equals to -1
scan[0] = max((int)cf, ps) & cf; // update carry flags within warps
cf = scan[-2];

if(thid_in_warp == 0)
    cf = ps;
if((int)cf < 0)
    cf = 0;

UADDO(a.x, a.x, cf) // propagate carry flag if needed
UADDC(a.y, a.y, 0)
UADDC(a.z, a.z, 0)
UADDC(a.w, a.w, 0)
((uint4 *)g_R)[thid] = a;
}

CUDA 4.0には対応する組み込み関数があるため、マクロ UADDO / UADDC はもう必要ないかもしれないことに注意してください(ただし、完全にはわかりません)。

また、並列リダクションは非常に高速ですが、複数の大きな整数を続けて加算する必要がある場合は、冗長な表現 (上記のコメントで提案されています) を使用する方が良いかもしれません。つまり、最初に加算の結果を64 ビット ワードを実行し、最後に「1 回の掃引」で 1 回のキャリー伝搬を実行します。

于 2012-10-21T18:35:14.817 に答える
4

@asmに加えて、私の回答も投稿すると思ったので、このSOの質問は一種のアイデアのリポジトリになる可能性があります。@asm と同様に、キャリー条件と「キャリースルー」条件を検出して保存します。中間ワードの結果がすべて 1 (0xF...FFF) の場合、キャリーがこのワードに伝播すると、次のワードに「キャリースルー」されます。

コードで PTX や asm を使用しなかったため、1024 スレッドを使用して 2048x32 ビットの機能を実現するために、32 ビットではなく 64 ビットの unsigned int を使用することにしました。

@asm のコードとの大きな違いは、私の並列キャリー伝搬スキームにあります。各ビットが、1024 個のスレッドのそれぞれからの独立した中間 64 ビット加算から生成されたキャリー条件を表す、ビットパック配列 (「キャリー」) を作成します。また、各ビットが個々の 64 ビット中間結果のキャリースルー条件を表すビットパック配列 (「キャリースルー」) を作成します。1024 スレッドの場合、これはビットパック配列ごとに共有メモリの 1024/64 = 16x64 ビット ワードになるため、共有メモリの合計使用量は 64+3 32 ビット量になります。これらのビット パック配列を使用して、次の手順を実行して、結合された伝搬キャリー インジケーターを生成します。

carry = carry | (carry_through ^ ((carry & carry_through) + carry_through);

(キャリーは 1 つ左にシフトされることに注意してください。キャリー [i] は、a[i-1] + b[i-1] の結果がキャリーを生成したことを示します) 説明は次のとおりです。

  1. キャリーとキャリースルーのビット単位 and は、キャリーが一連の 1 つ以上のキャリースルー条件と相互作用する候補を生成します。
  2. ステップ1の結果をcarry_throughに追加すると、carry_throughシーケンスへのキャリーの伝播によって影響を受けるすべてのワードを表すビットが変更された結果が生成されます
  3. キャリースルーとステップ 2 の結果の排他的論理和を取ると、影響を受ける結果が 1 ビットで示されます。
  4. 手順 3 の結果と通常のキャリー インジケーターのビット単位の OR を取得すると、結合されたキャリー条件が得られ、それを使用してすべての中間結果が更新されます。

ステップ 2 の加算では、別のマルチワード加算が必要であることに注意してください (64 ワードを超える big int の場合)。私はこのアルゴリズムが機能すると信じており、私が投げたテスト ケースに合格しました。

これを実装する私のコード例は次のとおりです。

// parallel add of large integers
// requires CC 2.0 or higher
// compile with:
// nvcc -O3 -arch=sm_20 -o paradd2 paradd2.cu
#include <stdio.h>
#include <stdlib.h>

#define MAXSIZE 1024 // the number of 64 bit quantities that can be added
#define LLBITS 64  // the number of bits in a long long
#define BSIZE ((MAXSIZE + LLBITS -1)/LLBITS) // MAXSIZE when packed into bits
#define nTPB MAXSIZE

// define either GPU or GPUCOPY, not both -- for timing
#define GPU
//#define GPUCOPY

#define LOOPCNT 1000

#define cudaCheckErrors(msg) \
    do { \
        cudaError_t __err = cudaGetLastError(); \
        if (__err != cudaSuccess) { \
            fprintf(stderr, "Fatal error: %s (%s at %s:%d)\n", \
                msg, cudaGetErrorString(__err), \
                __FILE__, __LINE__); \
            fprintf(stderr, "*** FAILED - ABORTING\n"); \
            exit(1); \
        } \
    } while (0)

// perform c = a + b, for unsigned integers of psize*64 bits.
// all work done in a single threadblock.
// multiple threadblocks are handling multiple separate addition problems
// least significant word is at a[0], etc.

__global__ void paradd(const unsigned size, const unsigned psize, unsigned long long *c, const unsigned long long *a, const unsigned long long *b){

  __shared__ unsigned long long carry_through[BSIZE];
  __shared__ unsigned long long carry[BSIZE+1];
  __shared__ volatile unsigned mcarry;
  __shared__ volatile unsigned mcarry_through;

  unsigned idx = threadIdx.x + (psize * blockIdx.x);
  if ((threadIdx.x < psize) && (idx < size)){
    // handle 64 bit unsigned add first
    unsigned long long cr1 = a[idx];
    unsigned long long lc = cr1 + b[idx];
    // handle carry
    if (threadIdx.x < BSIZE){
      carry[threadIdx.x] = 0;
      carry_through[threadIdx.x] = 0;
      }
    if (threadIdx.x == 0){
      mcarry = 0;
      mcarry_through = 0;
      }
    __syncthreads();
    if (lc < cr1){
      if ((threadIdx.x%LLBITS) != (LLBITS-1))  
        atomicAdd(&(carry[threadIdx.x/LLBITS]), (2ull<<(threadIdx.x%LLBITS)));
      else atomicAdd(&(carry[(threadIdx.x/LLBITS)+1]), 1);
      }
    // handle carry-through
    if (lc == 0xFFFFFFFFFFFFFFFFull) 
      atomicAdd(&(carry_through[threadIdx.x/LLBITS]), (1ull<<(threadIdx.x%LLBITS))); 
    __syncthreads();
    if (threadIdx.x < ((psize + LLBITS-1)/LLBITS)){
      // only 1 warp executing within this if statement
      unsigned long long cr3 = carry_through[threadIdx.x];
      cr1 = carry[threadIdx.x] & cr3;
      // start of sub-add
      unsigned long long cr2 = cr3 + cr1;
      if (cr2 < cr1) atomicAdd((unsigned *)&mcarry, (2u<<(threadIdx.x)));
      if (cr2 == 0xFFFFFFFFFFFFFFFFull) atomicAdd((unsigned *)&mcarry_through, (1u<<threadIdx.x));
      if (threadIdx.x == 0) {
        unsigned cr4 = mcarry & mcarry_through;
        cr4 += mcarry_through;
        mcarry |= (mcarry_through ^ cr4); 
        }
      if (mcarry & (1u<<threadIdx.x)) cr2++;
      // end of sub-add
      carry[threadIdx.x] |= (cr2 ^ cr3);
      }
    __syncthreads();
    if (carry[threadIdx.x/LLBITS] & (1ull<<(threadIdx.x%LLBITS))) lc++;
    c[idx] = lc;
  }
}

int main() {

  unsigned long long *h_a, *h_b, *h_c, *d_a, *d_b, *d_c, *c;
  unsigned at_once = 256;   // valid range = 1 .. 65535
  unsigned prob_size = MAXSIZE ; // valid range = 1 .. MAXSIZE
  unsigned dsize = at_once * prob_size;
  cudaEvent_t t_start_gpu, t_start_cpu, t_end_gpu, t_end_cpu;
  float et_gpu, et_cpu, tot_gpu, tot_cpu;
  tot_gpu = 0;
  tot_cpu = 0;


  if (sizeof(unsigned long long) != (LLBITS/8)) {printf("Word Size Error\n"); return 1;}
  if ((c = (unsigned long long *)malloc(dsize * sizeof(unsigned long long)))  == 0) {printf("Malloc Fail\n"); return 1;}

  cudaHostAlloc((void **)&h_a, dsize * sizeof(unsigned long long), cudaHostAllocDefault);
  cudaCheckErrors("cudaHostAlloc1 fail");
  cudaHostAlloc((void **)&h_b, dsize * sizeof(unsigned long long), cudaHostAllocDefault);
  cudaCheckErrors("cudaHostAlloc2 fail");
  cudaHostAlloc((void **)&h_c, dsize * sizeof(unsigned long long), cudaHostAllocDefault);
  cudaCheckErrors("cudaHostAlloc3 fail");

  cudaMalloc((void **)&d_a, dsize * sizeof(unsigned long long));
  cudaCheckErrors("cudaMalloc1 fail");
  cudaMalloc((void **)&d_b, dsize * sizeof(unsigned long long));
  cudaCheckErrors("cudaMalloc2 fail");
  cudaMalloc((void **)&d_c, dsize * sizeof(unsigned long long));
  cudaCheckErrors("cudaMalloc3 fail");
  cudaMemset(d_c, 0, dsize*sizeof(unsigned long long));

  cudaEventCreate(&t_start_gpu);
  cudaEventCreate(&t_end_gpu);
  cudaEventCreate(&t_start_cpu);
  cudaEventCreate(&t_end_cpu);

  for (unsigned loops = 0; loops <LOOPCNT; loops++){
  //create some test cases
  if (loops == 0){
  for (int j=0; j<at_once; j++)
  for (int k=0; k<prob_size; k++){
    int i= (j*prob_size) + k;
    h_a[i] = 0xFFFFFFFFFFFFFFFFull;
    h_b[i] = 0;
    }
    h_a[prob_size-1] = 0;
    h_b[prob_size-1] = 1;
    h_b[0] = 1;
  }
  else if (loops == 1){
  for (int i=0; i<dsize; i++){
    h_a[i] = 0xFFFFFFFFFFFFFFFFull;
    h_b[i] = 0;
    }
    h_b[0] = 1;
  }
  else if (loops == 2){
  for (int i=0; i<dsize; i++){
    h_a[i] = 0xFFFFFFFFFFFFFFFEull;
    h_b[i] = 2;
    }
    h_b[0] = 1;
  }
  else {
  for (int i = 0; i<dsize; i++){
    h_a[i] = (((unsigned long long)lrand48())<<33) + (unsigned long long)lrand48();
    h_b[i] = (((unsigned long long)lrand48())<<33) + (unsigned long long)lrand48();
    }
  }
#ifdef GPUCOPY
  cudaEventRecord(t_start_gpu, 0);
#endif
  cudaMemcpy(d_a, h_a, dsize*sizeof(unsigned long long), cudaMemcpyHostToDevice);
  cudaCheckErrors("cudaMemcpy1 fail");
  cudaMemcpy(d_b, h_b, dsize*sizeof(unsigned long long), cudaMemcpyHostToDevice);
  cudaCheckErrors("cudaMemcpy2 fail");
#ifdef GPU
  cudaEventRecord(t_start_gpu, 0);
#endif
  paradd<<<at_once, nTPB>>>(dsize, prob_size, d_c, d_a, d_b);
  cudaCheckErrors("Kernel Fail");
#ifdef GPU
  cudaEventRecord(t_end_gpu, 0);
#endif
  cudaMemcpy(h_c, d_c, dsize*sizeof(unsigned long long), cudaMemcpyDeviceToHost);
  cudaCheckErrors("cudaMemcpy3 fail");
#ifdef GPUCOPY
  cudaEventRecord(t_end_gpu, 0);
#endif
  cudaEventSynchronize(t_end_gpu);
  cudaEventElapsedTime(&et_gpu, t_start_gpu, t_end_gpu);
  tot_gpu += et_gpu;
  cudaEventRecord(t_start_cpu, 0);
  //also compute result on CPU for comparison
  for (int j=0; j<at_once; j++) {
    unsigned rc=0;
    for (int n=0; n<prob_size; n++){
      unsigned i = (j*prob_size) + n;
      c[i] = h_a[i] + h_b[i];
      if (c[i] < h_a[i]) {
        c[i] += rc;
        rc=1;}
      else {
        if ((c[i] += rc) != 0) rc=0;
        }
      if (c[i] != h_c[i]) {printf("Results mismatch at offset %d, GPU = 0x%lX, CPU = 0x%lX\n", i, h_c[i], c[i]); return 1;}
      }
    }
  cudaEventRecord(t_end_cpu, 0);
  cudaEventSynchronize(t_end_cpu);
  cudaEventElapsedTime(&et_cpu, t_start_cpu, t_end_cpu);
  tot_cpu += et_cpu;
  if ((loops%(LOOPCNT/10)) == 0) printf("*\n");
  }
  printf("\nResults Match!\n");
  printf("Average GPU time = %fms\n", (tot_gpu/LOOPCNT));
  printf("Average CPU time = %fms\n", (tot_cpu/LOOPCNT));

  return 0;
}
于 2012-10-21T21:17:44.297 に答える