14.4. HelloWorld(データ並列プログラミング)

Note

データ並列プログラミングの詳細については「the section called “データ並列プログラミングモデル”」、「the section called “カーネルの実行”」、「Section 13.18, “NDRange”」、「Section 13.17, “ワークグループ”」、「Section 17.4.1, “ワークアイテム関数”」を参照ください。

データ並列プログラミングについては、タスク並列プログラミングと異なり、clEnqueueNDRangeKernel関数を使い、グローバル・ローカル空間の次元やサイズを指定する必要があります。

この実装例では「2 x 2」の行列の乗算をしてみます。式に起こすと、「A x B = C」とし、AとBを掛けてCを計算します。A、B、CはNxM(2 x 2)の行列とします。正確には、Aは(NxP)、Bは(PxM)、Cは(NxM)の実数空間とします。AとBは入力、Cは出力に使います。(つまりP=2、N=M=2)

行列はバッファオブジェクトとしてグローバルメモリ領域に保管します。このサンプルでは、ホストポインタを使った前の項目に対して、バッファの生成時は領域を空にして、バッファの書き込みコマンド(clEnqueueWriteBuffer関数)をキューに挿入します。また、出力行列Cについては、バッファの読み込みコマンド(clEnqueueReadBuffer関数)を使い演算結果を取得します。

またOpenCLのプロファイルフラグを有効とし、clGetEventProfilingInfo関数を使いカーネル実行時間を採集します。

MultiplicationTest.py. 

import pyopencl as cl
import numpy as np

data = np.arange(16).astype(np.int32)

devices = [cl.get_platforms()[0].get_devices(cl.device_type.GPU)[0]]

ctx = cl.Context(devices)
queue = cl.CommandQueue(ctx)
mf = cl.mem_flags
data_mem = cl.Buffer(ctx, mf.USE_HOST_PTR, hostbuf=data)

MAT_DIM = 2
WORK_DIM = 2 #(1)

# 次元の設定:ここではsquare(N*N)にします。

m_dim = MAT_DIM #(2)
n_dim = MAT_DIM #(3)
p_dim = MAT_DIM #(4)

# A in M by P : B in P by N : C in M by N dimensional spaces */

a_size = n_dim * p_dim #(5)
b_size = p_dim * m_dim #(6)
c_size = n_dim * m_dim #(7)

global_size = (WORK_DIM, WORK_DIM, ) #(8)

a = np.arange(a_size).astype(np.float32) #(9)
b = np.arange(b_size).astype(np.float32) #(10)
c = np.arange(c_size).astype(np.float32) #(11)

print("a: " + str(a))
print("b: " + str(b))

a_mem = cl.Buffer(ctx, cl.mem_flags.USE_HOST_PTR, hostbuf=a) #(12)
b_mem = cl.Buffer(ctx, cl.mem_flags.USE_HOST_PTR, hostbuf=b) #(13)
c_mem = cl.Buffer(ctx, cl.mem_flags.USE_HOST_PTR, hostbuf=c) #(14)

program = cl.Program(ctx, """
    __kernel void mult(
        const int m_dim,
        const int n_dim,
        const int p_dim,
        global float* A,
        global float* B,
        global float* C)
    {
        int k;
        int i = get_global_id(0);
        int j = get_global_id(1);
        float tmp;
        if((i < n_dim) && (j < m_dim)) {
            tmp = 0.0f;
            for(k=0; k<p_dim;k++) {
                tmp += A[i * n_dim + k] * B[k * p_dim + j];
            }
            C[i * n_dim + j] = tmp;
        }
    }
    """).build()

kernel = cl.Kernel(program, name="mult") #(15)

kernel.set_arg(0, np.int32(m_dim)) #(16)
kernel.set_arg(1, np.int32(n_dim)) #(17)
kernel.set_arg(2, np.int32(p_dim)) #(18)
kernel.set_arg(3, a_mem) #(19)
kernel.set_arg(4, b_mem) #(20)
kernel.set_arg(5, c_mem) #(21)

cl.enqueue_nd_range_kernel( #(22)
    queue, #(23)
    kernel, #(24)
    global_work_size=global_size, #(25)
    local_work_size=(1, 1, )) #(26)

out = np.ndarray(c_size).astype(np.float32) #(27)

cl.enqueue_read_buffer(queue, mem=c_mem, hostbuf=out) #(28)

print("OpenCL Output: " + str(out))

mat = np.array([[0.0, 1.0], [2.0, 3.0]])

out_check = np.dot(mat, mat)

print("Numpy Output: " + str(out_check))

(1)

global_size(グローバル空間のサイズ)の次元を2次元に設定。

(2)

Mを設定。

(3)

Nを設定。

(4)

Pを設定。

(5)

Aの要素数を計算。

(6)

Bの要素数を計算。

(7)

Cの要素数を計算。

(8)

グローバル空間の次元数を指定。

(9)

行列Aのメモリ領域を確保。

(10)

行列Bのメモリ領域を確保。

(11)

行列Cのメモリ領域を確保。

(12)

Aのバッファオブジェクトを宣言。

(13)

Bのバッファオブジェクトを宣言。

(14)

Cのバッファオブジェクトを宣言。

(15)

カーネルオブジェクトのインスタンスを生成します。nameはカーネル関数に記述した関数名を指定します。この場合一つしかありませんが、複数あれば使いたい関数を指定します。

(16)

引数0を指定。ポインタでない場合、numpyの値をそのまま代入可能。

(17)

引数1を指定。ポインタでない場合、numpyの値をそのまま代入可能。

(18)

引数2を指定。ポインタでない場合、numpyの値をそのまま代入可能。

(19)

引数3を指定。バッファオブジェクトを指定します。

(20)

引数4を指定。バッファオブジェクトを指定します。

(21)

引数5を指定。バッファオブジェクトを指定します。

(22)

enqueue_nd_range_kernelをコマンドキューに挿入して、カーネルオブジェクトを実行します。

(23)

コマンドキューを指定。

(24)

カーネルオブジェクトを指定。

(25)

グローバルインデックス空間(2,2,)を指定。

(26)

ローカルインデックス空間(1,1,)を指定。

(27)

出力配列を初期化。

(28)

enqueue_read_bufferを使って、デバイスからホストメモリ内にデータを読み込みます。

カーネル関数. 

__kernel void mult(
        const int m_dim, //(1)
        const int n_dim, //(2)
        const int p_dim, //(3)
        __global float* A, //(4)
        __global float* B, //(5)
        __global float* C) //(6)
{
        int k;
        int i = get_global_id(0); //(7)
        int j = get_global_id(1); //(8)
        float tmp;
        if((i < n_dim) && (j < m_dim)) {
                tmp = 0.0f;
                for(k=0; k<p_dim;k++) {
                        tmp += A[i * n_dim + k] * B[k * p_dim + j]; //(9)
                }
        C[i * n_dim + j] = tmp; //(10)
        }
}

(1)

ホストプログラムで指定したm_dim変数

(2)

ホストプログラムで指定したn_dim変数

(3)

ホストプログラムで指定したp_dim変数

(4)

Aのバッファオブジェクト

(5)

Bのバッファオブジェクト

(6)

Cのバッファオブジェクト

(7)

次元0のグローバルIDをiに代入

(8)

次元1のグローバルIDをjに代入

(9)

A(i,k)* B(k,j)を計算。

(10)

C(i,j)を計算。

出力. 

a: [ 0.  1.  2.  3.]
b: [ 0.  1.  2.  3.]
OpenCL Output: [  2.   3.   6.  11.]
Numpy Output: [[  2.   3.]
 [  6.  11.]]

/* A * B = C
 *
 *   | 0   1 | * | 0   1 | = | 2  3  |
 *   | 2   3 |   | 2   3 |   | 6  11 |
 *
 */

上記の出力は想定結果と一致することが確認できます。

Note

この項目のプログラムではワークグループは一つの最もシンプルな設計となります。ワークグループを複数とする実装例を見るのだれば「ワークグループ実装例」(the section called “実装例”)を参照ください。

14.4.1. 例外クラス

エラー発生時にPyOpenCLでは以下のErrorを継承した例外が投げられます。

class pyopencl.Error

このErrorクラスを継承したのが以下の3つのクラスです。

class pyopencl.MemoryError
class pyopencl.LogicError
class pyopencl.RuntimeError

PyOpenCLのエラーはOpenCLのエラーをPythonの例外クラスにマップしたものとなります。

詳細については「表:ランタイムエラー一覧」(Table C.1, “表:ランタイムエラー一覧”)と「表:コンパイルタイムエラー一覧」(Table C.2, “表:コンパイルタイムエラー一覧”)を参照ください。

Warning

本書ではコード行数が肥大するため、以降のサンプルコードではエラーコードの処理はしていません。

Copyright 2018-2019, by Masaki Komatsu