汎用GPUにおける結合荷重及び関連値の確保と保持

題名:汎用GPUを利用するにあたっての、結合荷重及び関連値の確保と保持についての解説
著者:柏木 明博
作成日:2017年6月14日

不連続な領域としてではなく、連続した領域としてメモリを確保する

ここでは、汎用GPUを利用するにあたって、結合荷重 \(w\) やバイアス \(b\) 、 出力 \(z\) や誤差 \(d\) の確保と保持の仕方について、説明します。使用する 汎用GPUは、デファクトスタンダードでもあるnVIDIA、コンパイラはCUDAを使用します。

CPU上では、適ほど構造体を定義し、その構造体の要素としてポインタを用意すること で、インスタンス作成時にそれぞれ必要なだけ、malloc()することができます。この 状態でそのポインタの実体に対して計算を行えば利用可能ですが、メモリ上は連続し た領域にメモリが確保されておらず、断片化しています。汎用GPUを利用する場合には、 使用するメモリ領域を汎用GPUとCPUとの間で転送しなければなりませんが、断片化し た状態では転送が非常に煩雑となり、処理速度にも影響してきます。転送回数を減ら すため、連続したメモリ領域を利用できるように工夫が必要です。

今回は、Neural Networkの層数と、各層のユニット数から、必要なメモリ量を事前に 計算し、それを一回限りの malloc()で確保することで、汎用GPUとの転送の効率化を 図っています。

一覧表1.データ構造

  1. 構造体への保持用領域
  2. 全体の層数 \(l\) 保持用領域
  3. 層ごとの 出力値 \(z\) の数 保持領域
  4. 層ごとの バイアス値 \(b\) の数 保持領域
  5. 層ごとの 結合加重 \(w\) の数 保持領域
  6. 層ごとの 誤差 \(d\) の数 保持領域
  7. 層ごとの バイアス誤差 \(db\) の数 保持領域
  8. 層ごとの実際の 出力値 \(z\) 保持領域
  9. 層ごとの実際の バイアス値 \(b\) 保持領域
  10. 層ごとの実際の 結合加重 \(w\) 保持領域
  11. 層ごとの実際の 誤差 \(d\) 保持領域
  12. 層ごとの実際の バイアス誤差 \(db\) 保持領域

一覧表1に挙げた各要素に必要なメモリ領域を加算し、合計値でmalloc()する事で、 連続した領域を確保します。具体的なコードで示すと、以下のようにります。

LIST 1. NEUTON_T構造体

                                // Neuron structure
typedef struct neuron_t{

        double **z;
                                // value  of z
        long *z_num;
                                // number of z
        double **b;
                                // value  of b
        long *b_num;
                                // number of b
        double **w;
                                // value  of w
        long *w_num;
                                // number of w
        double **d;
                                // value  of d
        long *d_num;
                                // number of d
        double **db;
                                // value  of db
        long *db_num;
                                // number of db
} NEURON_T;

LIST 2. 引数と変数宣言

long alloc_mem(
          long  l_num,
          long *z_num,
          long *b_num,
          long *w_num,
          long *d_num,
          long *db_num,
        void **mem_cpu,
        void **mem_dev,
        void **train_cpu,
        void **train_dev,
          long train_num,
        void **teach_cpu,
        void **teach_dev,
          long teach_num
){
        cudaError_t err;
                                // Error code of cuda
        long phase;
                                // Number of phase
        long size;
                                // Size of memory
        long cur;
                                // Cursor
        long *mem_cpu_p;

        NEURON_T *n;

LIST 3. サイズ計算

                                // Long type pointer of cpu side memory
size = 0;
                                // Init value of size

                                // Add number of size of NEURON_T
size += sizeof(NEURON_T);
                                // Add number of size of l_num
size += sizeof(long);
                                // Add number of 1).z_num, 2).b_num,
                                // 3).w_num, 4).d_num, 5).db_num
size += sizeof(long) * 5 * l_num;

for(phase = 0; phase < l_num; phase++){
                                // Calculate an all size
        size += sizeof(double) * (
                   z_num[phase]
                +  b_num[phase]
                +  w_num[phase]
                +  d_num[phase]
                + db_num[phase]
        );
}

ここでは、一覧表1に挙げた順にメモリサイズを計算しています。まず、0でリセットし、 先頭部分にNEURON_T構造体のサイズ分確保します。そして、層数、各層における \(z,b,w,d,db\) の数を保存する為のサイズを確保し、 \(z,b,w,d,db\) の実際 の値を保存するサイズを加算します。

LIST 4. メモリ確保

*mem_cpu = (void *)malloc( size );
                                // Memory allocate at CPU
if( mem_cpu == NULL ){
        return( -1 );
}

err = cudaMalloc( (void**)&(*mem_dev), size );
                                // Memory allocate at GPU
if( err != cudaSuccess){
        return( -2 );
}

計算して得た必要なメモリサイズを用いて、CPU側と汎用GPU側それぞれに連続した領 域を確保します。連続した領域は、一覧表1の先頭「1.構造体への保持用領域」の要 素である各値へのポインタへ再割り当てすることで、使用可能となります。再割り当 て処理は、汎用GPU上においても、CPU上でも同様です。

LIST 4.1は、このセクションでは必要ありませんが、後の項である「Back Propagation 」で必要になってくるため、ここで挙げておきます。

LIST 4.1. その他のメモリの確保

*train_cpu = (void *)malloc(
        sizeof(double) * z_num[0] * train_num
);
                                // Train memory allocate at CPU
if( train_cpu == NULL ){
        return( -3 );
}

err = cudaMalloc( (void**)&(*train_dev),
        sizeof(double) * z_num[0] * train_num
);
                                // Train memory allocate at GPU
if( err != cudaSuccess){
        return( -4 );
}

*teach_cpu = (void *)malloc(
        sizeof(double) * z_num[l_num-1] * teach_num
);
                                // Teach memory allocate at CPU
if( teach_cpu == NULL ){
        return( -5 );
}

err = cudaMalloc( (void**)&(*teach_dev),
        sizeof(double) * z_num[l_num-1] * teach_num
);
                                // Teach memory allocate at GPU
if( err != cudaSuccess){
        return( -6 );
}

始めに述べた「一覧表1.データ構造」のNo.2からNo.6は、モデルの層数とz,b,w,d,db 各値の層ごとの数を格納する領域となっています。ここでは、その値を設定します。

LIST 5. 層数と各値の数の格納

                                // Init a cursor
cur = 0;

n = (NEURON_T *)*mem_cpu;
cur++;
                                // Store a pointer address
                                // mem_cpu_p = (long *)*mem_cpu;
mem_cpu_p = (long *)&n[cur];
                                // Init a cursor
cur = 0;
                                // Set number of phases
mem_cpu_p[cur] = l_num;
cur++;
                                // Set number of each array
for(phase = 0; phase < l_num; phase++){

        mem_cpu_p[cur] = z_num[phase];
        cur++;
                                // For z_num
}

for(phase = 0; phase < l_num; phase++){

        mem_cpu_p[cur] = b_num[phase];
        cur++;
                                // For b_num
}

for(phase = 0; phase < l_num; phase++){

        mem_cpu_p[cur] = w_num[phase];
        cur++;
                                // For w_num
}

for(phase = 0; phase < l_num; phase++){

        mem_cpu_p[cur] = d_num[phase];
        cur++;
                                // For d_num
}

for(phase = 0; phase < l_num; phase++){

        mem_cpu_p[cur] = db_num[phase];
        cur++;
                                // For db_num
}

return size;
                                // Normal Terminate

構造体への再割当て

各種計算用関数から利用し易いように、構造体への再割り当てを行います。再割り当 ては、上記「一覧表1.構造体要素」を再計算し、それぞれの保持領域への先頭アドレ スを「1.構造体への保持用領域」の要素である各値へのポインタへ格納し直します。

LIST 6. 引数の取得

__device__ __host__ NEURON_T *set_instance(
        long    l_num,
        void **mem
){

LIST 7. 変数宣言

NEURON_T *n;
                                // Pointer of liner memory for long
long *mem_long;
                                // Pointer of liner memory for double
double *mem_double;
                                // Counter for cursor
long phase_len;
                                // Counter for phase
long phase;

LIST 8. メモリ領域の確保

                                // Init a length at each phase
phase_len = 0;
                                // Set address of top
n = (NEURON_T *)*mem;
                                // Increment cursor
phase_len++;
                                // allocate memory for z,b,w,d,db
n->z_num  = (long *)malloc( sizeof(long) * l_num );
n->b_num  = (long *)malloc( sizeof(long) * l_num );
n->w_num  = (long *)malloc( sizeof(long) * l_num );
n->d_num  = (long *)malloc( sizeof(long) * l_num );
n->db_num = (long *)malloc( sizeof(long) * l_num );

n->z  = (double**)malloc( sizeof(double*) * l_num);
n->b  = (double**)malloc( sizeof(double*) * l_num);
n->w  = (double**)malloc( sizeof(double*) * l_num);
n->d  = (double**)malloc( sizeof(double*) * l_num);
n->db = (double**)malloc( sizeof(double*) * l_num);

LIST 9. メモリアドレスのポインタへの再割当て

                                // Set pointer address
                                //                 for long array again
mem_long = (long *)&n[phase_len];
                                //mem_long = (long *)*mem;

                                // Initialize a cursor
phase_len = 0;
                                // Get number of phases of this network
l_num = mem_long[phase_len];
                                // Increment pointer
phase_len++;

for(phase = 0; phase < l_num; phase++){

                                // Get number of z at each phase
        n->z_num[phase] = mem_long[phase_len];
        phase_len++;
}

for(phase = 0; phase < l_num; phase++){

                                // Get number of b at each phase
        n->b_num[phase] = mem_long[(phase_len)];
        phase_len++;
}

for(phase = 0; phase < l_num; phase++){

                                // Get number of w at each phase
        n->w_num[phase] = mem_long[(phase_len)];
        phase_len++;
}

for(phase = 0; phase < l_num; phase++){

                                // Get number of d at each phase
        n->d_num[phase] = mem_long[(phase_len)];
        phase_len++;
}

for(phase = 0; phase < l_num; phase++){

                                // Get number of db at each phase
        n->db_num[phase] = mem_long[(phase_len)];
        phase_len++;
}
                                // Set pointer address for long array
mem_double = (double *)&mem_long[phase_len];

                                // Initialize a cursor
phase_len = 0;

for( phase = 0; phase < l_num; phase++ ){

                                // Set pointer to an each variables

        n->z[phase] = &mem_double[(phase_len) + 0];
                                // for z

        n->b[phase] = &mem_double[(phase_len) + n->z_num[phase]];
                                // for b

        n->w[phase] = &mem_double[
                (phase_len) + n->z_num[phase] + n->b_num[phase]
        ];
                                // for w

        n->d[phase] = &mem_double[
                (phase_len)
                + n->z_num[phase]
                + n->b_num[phase]
                + n->w_num[phase]
        ];                      // for delta

        n->db[phase] = &mem_double[
                (phase_len)
                + n->z_num[phase]
                + n->b_num[phase]
                + n->w_num[phase]
                + n->d_num[phase]
        ];                      // for delta of bias

        phase_len
                += n->z_num[phase]
                +  n->b_num[phase]
                +  n->w_num[phase]
                +  n->d_num[phase]
                +  n->db_num[phase];
                                // Calculate a size of each phase
}

LIST 8の先頭部分で、NEURON_T構造体のアドレスを(long *)でキャストしてmem_long ポインタへ代入していますが、お分かりの通り、phase_lenの値は加算されて1となっ ているため、一覧表1の「1.構造体への保持用領域」の次の要素である「2.全体の層数 \(l\) 保持領域」を指しています。この層数 \(l\) を格納しているl_numは long型の為、わざわざポインタをキャストしてlong型として取り出せるようにしてい ます。C言語のポインタマジックです。そして、phase_lenを0でリセットした後、改め てmem_long[]の先頭アドレスの値をl_numへ代入し、層数 \(l\) を取り出します。 この関数set_instance()の引数void **memには、l_numの位置に層数がセットされて引 き渡されてきます。以降、 \(z,b,w,d,db\) の各層の数を順に取り出してNEURON_T 構造体へ代入して行きます。途中、double型のmem_doubleポインタへキャストしてい るところがありますが、これも先ほど説明した通り、double型の値を取り出すために、 わざわざ(double *)でキャストしています。以降、 \(z,b,w,d,db\) の値を取り出 してNEURON_T構造体へ代入しています。

以上、メモリ転送を考慮した、汎用GPUとCPUにおける連続したメモリ領域の確保と保 持についての解説です。