.. tutorial. Tutorial ======== 本チュートリアルでは、以下に示すようなVPPLプログラムを書けるように、VPPLの概要を解説していきます。 詳しい文法は、 :doc:`grammer` ページを参照してください。 直接VPPLプログラムを書かず、Python上のスクリプトとして記述してこれを実行することでVPPLプログラムを生成、実行する方法は :doc:`agent` ページを参照してください。 また、DNNダイアグラムからVPPLプログラムを生成、実行する方法は :doc:`dnnDev` ページを参照してください。 以下のプログラムは、QVP0という名のプロセスで簡単なベクトル計算を行って、その結果を別なコアで実行されている QVP1という名のプロセスに転送するものです。 各プロセスは物理的に異なるプロセッサコアに「必ず」割り当てて実行されます。 現状、プロセス=プロセッサコアと考えても間違いではありません。 ただし、将来的に1プロセッサコアで複数のプロセスを切り替えて動作させる機構を導入する予定なのであくまで現状です。 .. code-block:: none solution INTERPROCESS_COMMUNICATION_SAMPLE is parameter DEPTH = 3, REP_COUNT = 6; process __QVP0 = QVP0, __QVP1 = QVP1; fifo fifo0 from __QVP0 to __QVP1 depth DEPTH; global vFIFO : u8[32, DEPTH] = 0, vA : u8[32] = vseq[0, 1], vB : u8[32] = 1, vX : u8[32] = 0, fifoWrPtr : pointer, fifoRdPtr : pointer; # DATA SENDER proc QVP0; var oCounter : register; initialize init_fifo(); fifoWrPtr = `vFIFO; oCounter = 0 iteration isim_out$STRING("START QVP0"); isim_write(); while oCounter < REP_COUNT do vA = vA + vB; with syncFifoWrite fifo0 do $fifoWrPtr = vA end; next fifoWrPtr; isim_out$STRING("oCounter="); isim_out$I8(oCounter); isim_write(); isim_out$STRING("SEND DATA="); isim_out$VI8(vA); isim_write(); oCounter = oCounter + 1 end; join_all(100); exit_success() end QVP0 # DATA RECEIVER proc QVP1; var iCounter : register; initialize init_fifo(); fifoRdPtr = `vFIFO; iCounter = 0 iteration isim_out$STRING("START QVP1"); isim_write(); while iCounter < REP_COUNT do with syncFifoRead fifo0 do vX = $fifoRdPtr end; next fifoRdPtr; isim_out$STRING("iCounter="); isim_out$I8(iCounter); isim_write(); isim_out$STRING("RECEIVE DATA="); isim_out$VI8(vX); isim_write(); iCounter = iCounter + 1 end; exit_success() end QVP1 end INTERPROCESS_COMMUNICATION_SAMPLE VPPLコンパイラでコンパイル、アセンブル、ISIM実行までを行わせると、以下のような画面出力を得ることができます。 出力結果の左端の QCP1> QCP2>などは、ISIM上でのコア固有名です。 プログラムは32要素のベクタについて6回の演算と転送を行っており、その回数は出力側は oCounter, 入力側は iCounterで制御しています。 ベクタ演算自体は、元のベクタの各要素を+1する、というだけの単純なものです。 出力側では演算結果を共有メモリ上に実体を配置した3段のFIFOへ書き込んでおり、入力側ではそのFIFOから値を読み取っています。 出力側では、送信したデータをSEND DATA=以下に出力しており、入力側では、受信したデータをRECEIVE DATA=以下に出力します。 2つのコアのプロセスはいわば勝手に動作しているのですが、with文によってハードウエア的に同期がとられ 必ず一つデータ送信が完了してからデータ受信が起こっていることが分かります。 .. code-block:: none $ vppl -i -a --ISIM vecT101.vppl VPPL Compiler v01r02 (C) 2016 TopsSystems Source file: 72 Line(s) Compile OK. Assemble OK. ISIM Execution OK. ISIM Result Verifier: workbatch.LOG QCP1> START QVP0 QCP2> START QVP1 QCP1> oCounter=0 QCP2> iCounter=0 QCP1> SEND DATA=1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 QCP2> RECEIVE DATA=1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 QCP1> oCounter=1 QCP2> iCounter=1 QCP1> SEND DATA=2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 QCP2> RECEIVE DATA=2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 QCP1> oCounter=2 QCP2> iCounter=2 QCP1> SEND DATA=3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 QCP2> RECEIVE DATA=3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 QCP1> oCounter=3 QCP2> iCounter=3 QCP1> SEND DATA=4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 QCP2> RECEIVE DATA=4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 QCP1> oCounter=4 QCP2> iCounter=4 QCP1> SEND DATA=5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 QCP2> RECEIVE DATA=5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 QCP1> oCounter=5 QCP2> iCounter=5 QCP1> SEND DATA=6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 QCP2> RECEIVE DATA=6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 ISIM EXECUTION RESULT> SUCCESS Executed Cycles = 897 Start Time : 2016/03/15 13:13:19 Elapsed : 0:00:00:04.8171069 ソリューション宣言 ------------------ 複数のコアで実行されるプログラムは、ソリューションという単位でまとめられます。 最初にあらわれるのは、ソリューション宣言です。 .. code-block:: none solution INTERPROCESS_COMMUNICATION_SAMPLE is ソリューション名には適切な名前をつければよいですが、ソリューション末にも同じ識別子名が必要です。 .. code-block:: none end INTERPROCESS_COMMUNICATION_SAMPLE なお、ソリューションの外側で複数のソリューションをシステムにマップするレイヤーも検討中ですが、 現在はこのソリューションが最上位の構造です。 パラメータ宣言 -------------- パラメータ宣言は、マルチコアを制御する上で、あるいはコンパイルの過程に介入する上で重要な部分であるのですが、 ここでは単に、定数を宣言している部分だという理解で十分です。ここでは2つの定数 DEPTH と REP_COUNT を定義しています。 .. code-block:: none parameter DEPTH = 3, REP_COUNT = 6; ここで注意するべきは、「,」と「;」の使い分けです。これは、VPPLコンパイラの癖というべきですが、同じものを多数列挙する際には 「,」で区切り、文と文とは「;」で区切る、という原則です。 parameter 文は一つの文なので途中に「;」を書いてしまうとそこでおしまいです。 プロセス宣言 ------------ プロセス宣言は、物理的なコアとソフトウエア的なプロセスの紐づけをする部分であるとご理解ください。 ここの紐づけ方法には、静的、動的などいろいろ考えられるのですが、現状のVPPLコンパイラがサポートしているのは、静的な対応関係のみです。 .. code-block:: none process __QVP0 = QVP0, __QVP1 = QVP1; ここで「__」で始まっているのはコンパイラが内蔵している物理コア名であり、右辺の「QVP0」などはユーザーが命名できるプロセス名です。 FIFO宣言 ------------ FIFOは、プロセッサコア間に張られる通信チャネルです。1対1、1方通行のパイプのようなもので、入口から流し込んだデータを 出口で受け取ることができるようにするものです。プログラマブルで、任意のコア間にハードウエアで許される上限の数まで設定することが可能です。 コア間の受け渡しをFIFOとすることで、SMYLEvideo Gen.2のZOMP機構を使うことができます。ただし、ハードウエアで制御されるのは、 FIFOへのアクセスの制御だけで、データ保持の実体となるものは通常の共有メモリやレジスタにソフトウエアで準備しないとなりません。 FIFO宣言部で、どのコアとどのコアの間に深さがどれだけのFIFOがあり、その名前はなんなのか定義しないとなりません。 .. code-block:: none fifo fifo0 from __QVP0 to __QVP1 depth DEPTH; ここでは、fifo0という名のQVP0からQVP1への1個のFIFOのみが定義されており、その深さは DEPTHパラメータとなります。 global宣言 ---------- グローバル宣言部では、共有メモリ上にとられる大域変数を定義します。VPPL言語には種々の型がありますが、大きくわけると * スカラー型 * ベクタ型 の2つとなります。スカラー型は通常の言語の変数と同様ですが、ベクタ型は構造をもった2次元ベクトルを表現するためのネイティブな型で VPPLにおけるデータ処理の中心となるものです。FIFOの実体となる深さDEPTHのバッファ、ベクトル変数、ポインタ変数などはここで宣言します。 .. code-block:: none global vFIFO : u8[32, DEPTH] = 0, vA : u8[32] = vseq[0, 1], vB : u8[32] = 1, vX : u8[32] = 0, fifoWrPtr : pointer, fifoRdPtr : pointer; vFIFO変数は、FIFOの実体です。u8というのは符号なし整数型の要素のベクタで、そのX方向の幅は32、Y方向の深さはDEPTHとなります。 vA変数は、同じく32要素のu8要素からなるベクタで、各要素はコンパイラ初期化関数 vseqにより、初期値が0、差分1の等差数列として初期化されます。 vB, vXもベクタ変数ですが、その初期値はオール1およびオール0です。fifoWrPtrおよびfiroRdPtrはFIFOのアクセスに便利に使えるポインタ変数です。 プロセスQVP0の定義その1 ------------------------ プロセスQVP0の定義は以下のように始まります。 .. code-block:: none # DATA SENDER proc QVP0; var oCounter : register; #以下はコメントです。proc文により、まずプロセスの名前を示します。当然process宣言で宣言済の名前でなければなりません。 次に var 文により、プロセスのローカルかつ静的な変数を宣言します。ここでは、oCounterという名のレジスタ型の変数一個だけが 宣言されています。古代のCコンパイラではregister宣言は意味がありましたが、現代のCコンパイラではすでにその効果は失われています。 しかし、VPPL言語では頻繁に参照、更新されるローカル変数を扱う上で、register宣言は有効です。 プロセスQVP0の定義その2 ------------------------ 次に initialize というキーワードが登場します。initializeの直後に初期化、実際にはホットスタート時に実行されるべき 初期化コードを記述する複合文を配置します。ホットスタートは再起動時に一度だけ実行され、それ以外の繰り返しのトリガでは実行されないので、 記憶を維持したいものの最初の初期化をここで行います。 initializeは文ではなく、よって、initializeの後の実行文は「;」で区切っていきます。なお、最後の実行文の後には実行文がなく 次のキーワードが登場しなければならないので「;」を書くとコンパイルエラーとなります。この「癖」はPASCAL系の言語に共通のものです。 .. code-block:: none initialize init_fifo(); fifoWrPtr = `vFIFO; oCounter = 0 initializeの直後にある init_fifo() は、コンパイラ組み込みのインライン関数です。コンパイラ組み込み関数は実体のある機械語コードに変換されます。 この関数は、内部のfifo制御レジスタを初期化し、fifo制御を有効にするためのおまじないです。 次の行もFIFOアクセスのためです。 .. code-block:: none fifoWrPtr = `vFIFO; fifoWrPtrは、書き込むべきFIFOの実体を指すべきポインタなのですが、定義しただけではFIFOの実体がどこにあるのか知りません。 そこで、実体であるvFIFO変数と紐づけを行うわけです。ここで重要なのが「`」という記号です。コンパイラは、ベクタ変数が右辺に現れると ベクタキャッシュにそのデータをロードするようなコードを生成します。しかしここでは、ベクタ変数の保持するデータの実体ではなく vFIFO変数のアドレスなどがほしいので、この「`」(演算子ではなく修飾子ということになっています)をつけて紐づけします。 C言語のポインタ型に近いように思われるかもしれませんが、VPPL言語でC言語のポインタの概念により近いのはアドレス型です。 VPPL言語のポインタ型は、バッファオーバフローなど起こさないように大きさが管理され、かつ、ベクトル型の強みを生かすように設計されています。 .. code-block:: none oCounter = 0 これは、スカラー変数 oCounterに値を代入しているだけです。 プロセスQVP0の定義その3 ------------------------ 次にあらわれるのは、 iteration キーワードです。 interation も文ではなく、その後に実行用の複合文を置きます。 iteration部は、繰り返し発生するトリガ信号により起動がかかる実体処理を記述します。 トリガ信号などなく、ただ1回だけ処理する場合もここに書きます。 .. code-block:: none iteration isim_out$STRING("START QVP0"); isim_write(); 最初に現れているのは、コンパイラ組み込みインライン関数により、"START QVP0"という文字列をコンソールに出力する部分です。 この関数は \isim_ で始まっているとおり、ISIMモード(コンパイラオプション --ISIM)でコンパイルし、VPPLコンパイラをISIMドライバとして利用 (コンパイラオプション -i)したどきだけ意味を持ちます。FPGAなどでは別な関数があるのでそちらを使います。 次に while 文によるループがあります。oCounter が REP_COUNT定数より小さい間、do以下 endまでを繰り返し実行します。 ここで注目すべきは、 vA = vA + vB という次の文です。 vA、vBはベクタ型であり、この場合この一行で32要素の加算が完了します。 現状、VPPLコンパイラは最大512要素までのベクタ演算をループを使わぬSIMD命令に展開します。 .. code-block:: none while oCounter < REP_COUNT do vA = vA + vB; with syncFifoWrite fifo0 do $fifoWrPtr = vA end; next fifoWrPtr; isim_out$STRING("oCounter="); isim_out$I8(oCounter); isim_write(); isim_out$STRING("SEND DATA="); isim_out$VI8(vA); isim_write(); oCounter = oCounter + 1 end; 次に注目するのは、with文です。syncFifoWrite fifo0 という条件により、fifo0に空きがあるならば do 以降を実行します。 fifo0を読みだす側が遅れていて3段とったfifo0が満杯になってしまうと、この条件はみたされずハードウエアが実行をここでブロックします。 withブロック内にある $fifoWrPtr = vAにより、vAに書き込まれた演算結果が fifoWrPtrの指すFIFOの実体へと転送されます。 ポインタ変数の先頭に「$」修飾子をつけることで、ポインタ変数はポインタの指す先のベクトル変数のX方向1行に相当するようになります。 そしてendでブロックを〆ると、fifo0へ1段書き込まれたことがQVP1コアに伝達されます。 next fifoWrPtr文は、次の処理のためにfifoWrPtrを1段すすめる命令です。FIFO実体上はY方向に1行進めるという意味です。 FIFOに設定した段数の末尾にいたると自動的に先頭に戻るので、FIFO実体は一種のリングバッファとして使われることになります。 .. code-block:: none join_all(100); exit_success() end QVP0 このプログラムはQVP0からデータを送信し、QVP1でそれを受信し受け取って画面表示します。 ISIM上は、QVP0が終了するとすべて終了ということになるので(FPGA/RTLでは異なります)、QVP0が送信した直後に終了してしまうと QVP1での処理が中途半端になってしまう恐れがあります。そこで、join_all(100)コンパイラ組み込みインライン関数で、 QVP0以外が完了するまでQVP0を待たせます。その後の exit_success()コンパイラ組み込みインライン関数によりQVP0も終了します。 プロセスQVP1の定義 ------------------ データ受信側のQVP1の方は、ちょうどQVP0のREAD/WRITE裏返しのような形なので、QVP0をたどることでそのまま理解していただけると思います。 .. code-block:: none # DATA RECEIVER proc QVP1; var iCounter : register; initialize init_fifo(); fifoRdPtr = `vFIFO; iCounter = 0 iteration isim_out$STRING("START QVP1"); isim_write(); while iCounter < REP_COUNT do with syncFifoRead fifo0 do vX = $fifoRdPtr end; next fifoRdPtr; isim_out$STRING("iCounter="); isim_out$I8(iCounter); isim_write(); isim_out$STRING("RECEIVE DATA="); isim_out$VI8(vX); isim_write(); iCounter = iCounter + 1 end; exit_success() end QVP1