この記事は、インテル® デベロッパー・ゾーンに公開されている「opencl_node basic interfaces and opencl_buffer」(https://software.intel.com/en-us/blogs/2015/12/14/opencl-node-basic-interfaces-and-opencl-buffer/) の日本語参考訳です。
この記事は、インテル® スレッディング・ビルディング・ブロック (インテル® TBB) 4.4 Update 2 以降で利用可能な新しいノード opencl_node について説明するシリーズのパート 2 です。このノードは、インテル® TBB のフローグラフで OpenCL* デバイスの利用と連携を可能にします。このシリーズのパート 1 はこちらからご覧いただけます。
opencl_node の基本インターフェイス
以下は、opencl_node のジェネリック宣言です。
template< typename... PortTypes, typename Policy = queueing, typename Factory = default_opencl_factory > class opencl_node< tuple<PortTypes...>, Policy, Factory >;
opencl_node には入力ポートと出力ポートがあり、tuple<PortTypes…> でそれぞれの種類のポートを定義します。必須のテンプレート引数はこれだけです。
opencl_node は、join_node と同様のバッファーポリシーをサポートします。opencl_node で有効なポリシーは、queueing (デフォルト) と key_matching です。
第 3 引数の Factory は OpenCL* コンテキストを抽象化し、カーネル実行デバイスを指定します。デフォルトでは、opencl_node は default_opencl_factory で初期化されます。現在、default_opencl_factory はシステムで利用可能な最初の OpenCL* プラットフォームの最初のデバイスを選択します (将来、この動作は変更される可能性があります)。
入力ポートにメッセージが送られると、メッセージの関連データがデバイスで利用できるようになります。OpenCL* カーネルは、すべての入力ポートがメッセージを受け取ると実行されます。メッセージは異なるレートで入力ポートに送られますが、各ポートの合計メッセージ数は同じでなければなりません。カーネルに送られた後、メッセージはサクセサーへフォワードされます。
opencl_node のインスタンスを作成するには、少なくとも opencl_graph、opencl_program、およびカーネル名を作成時に指定する必要があります。
template <typename DeviceSelector> opencl_node( opencl_graph &g, const opencl_program<Factory> &p, const std::string &kernel_name, DeviceSelector d = /* デフォルト・デバイス・セレクター */, Factory &f = /* デフォルトの OpenCL* factory */ );
opencl_node には 1 つのカーネル名のみ指定できます。つまり、1 つの OpenCL* カーネルのフローグラフ抽象化のみ利用できます。アプリケーションで複数のカーネルを使用する場合は、複数の opencl_node オブジェクトを作成し、make_edge インターフェイスを用いてオブジェクト間の依存性を設定します。次に例を示します。
#define TBB_PREVIEW_FLOW_GRAPH_NODES 1 #include "tbb/flow_graph_opencl_node.h" #include <numeric> int main() { using namespace tbb::flow; opencl_graph g; opencl_node<tuple<cl_int>> cl1( g, "simple_dependency.cl", "k1" ); opencl_node<tuple<cl_int>> cl2( g, "simple_dependency.cl", "k2" ); opencl_node<tuple<cl_int>> cl3( g, "simple_dependency.cl", "k3" ); make_edge( output_port<0>(cl1), input_port<0>(cl2) ); make_edge( output_port<0>(cl1), input_port<0>(cl3) ); cl1.set_ndranges( { 1 } ); cl2.set_ndranges( { 1 } ); cl3.set_ndranges( { 1 } ); input_port<0>(cl1).try_put( 0 ); g.wait_for_all(); return 0; }
simple_dependency.cl:
kernel void k1( int b ) { printf("kernel #1\n"); } kernel void k2( int b ) { printf("kernel #2\n"); } kernel void k3( int b ) { printf("kernel #3\n"); }
上記のコードは、
kernel #1 kernel #2 kernel #3
または
kernel #1 kernel #3 kernel #2
を出力します。2 つ目と 3 つ目のカーネルは、1 つ目のカーネルに依存しますが、互いには依存しません。
make_edge( output_port<0>(cl1), input_port<0>(cl2) ); make_edge( output_port<0>(cl1), input_port<0>(cl3) );
そのため、カーネル k1 は常に最初に実行されますが、2 つ目と 3 つ目のカーネルの実行順序は非決定的です。
opencl_buffer
OpenCL* カーネルは、ホスト上に割り当てられた特殊なメモリー・オブジェクトを操作できます。フローグラフは、厳密な型の線形配列の抽象化である opencl_buffer テンプレート・クラスを提供します。
template <typename T, typename Factory = default_opencl_factory> class opencl_buffer { public: typedef /* 実装定義 */ iterator; // データアクセサー T* data(); iterator begin(); iterator end(); T& operator[] ( ptrdiff_t k ); size_t size(); // コンストラクター opencl_buffer( Factory &f, size_t size ); opencl_buffer( opencl_graph &g, size_t size ); };
バッファーの作成には、opencl_factory または opencl_graph が必要です。opencl_graph の場合、バッファーは (opencl_graph オブジェクトの) default_opencl_factory により作成されます。さらに、配列の要素数も指定する必要があります。次に例を示します。
opencl_graph g; const int N = 1000; opencl_buffer<cl_int> buf( g, N );
上記の例では、1000 要素の整数配列が作成されます。バッファーにデータを格納するには、次のようにホスト側でデータアクセサーを利用します。
std::iota( buf.begin(), buf.end(), 0 );
上記のコードは、0 から 999 の値をバッファーに格納します。OpenCL* デバイスでバッファーを並列に処理するには、同じサイズの ndrange を使用します。
std::vector<int> ndrange( 1, N ); clNode.set_ndranges( ndrange );
前述の例とは異なり、この例では ndrange に std::vector を使用しています。ベクトルには N と等しい 1 つの要素が含まれており、 set_ndrange に渡されると、1 次元の範囲 [0,N) として扱われます。
set_ndranges 関数には、begin() および end() メソッドを提供する任意のコンテナーを渡すことができます。std::initializer_list、std::vector, std::array を含む標準の C++ 型の多くは、これらのメソッドを提供しています。範囲の次元はコンテナーに含まれる要素数と同じで、各次元のサイズは対応する要素の値になります。
以下に、サンプルプログラム全体を示します。
#define TBB_PREVIEW_FLOW_GRAPH_NODES 1 #include "tbb/flow_graph_opencl_node.h" #include <numeric> #include <cassert> int main() { using namespace tbb::flow; opencl_graph g; const int N = 1000; opencl_buffer<cl_int> buf( g, N ); std::iota( buf.begin(), buf.end(), 0 ); opencl_node<tuple<opencl_buffer<cl_int>>> clNode( g, "opencl_buffer.cl", "increment" ); std::vector<int> ndrange( 1, N ); clNode.set_ndranges( ndrange ); input_port<0>(clNode).try_put( buf ); g.wait_for_all(); assert( std::accumulate( buf.begin(), buf.end(), 0 ) == N*(N+1)/2 ); return 0; }
opencl_buffer.cl:
kernel void increment ( global int* arr ) { const int i = get_global_id(0); arr[i] += 1;
このサンプルは、整数配列に 0 から 999 を格納し、opencl_node を用いて各要素を並列に 1 インクリメントして、assert で結果を確認します。
このシリーズの次の記事では、カーネルの実行に使用するデバイスの選択について述べます。
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください