データ駆動型のタスク レベル並列処理 - 2023.2 日本語

Vitis 高位合成ユーザー ガイド (UG1399)

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 日本語

データ駆動型のタスク レベル並列処理ではタスク チャネル モデリング方式が採用されており、タスクとチャネルを静的にインスタンシエートして明示的に接続する必要があります。タスクは、セマンティクスの呼び出し/戻りによって制御されず、入力ストリームのデータによって実行されています。このモデリング形式のタスクでは、通常ストリーム タイプの入力および出力のみが使用されます。ただし、データ駆動型 TLP の非同期 I/O で説明されるように、TLP デザインに配列への非同期ポインターとスカラー I/O を導入できます。

データ駆動型 TLP モデルは、処理するデータがあるときに実行されるタスクです。Vitis HLS の C シミュレーションの使用は、シーケンシャル セマンティクスと動作を確認する目的にのみ限られていました。データ駆動型モデルでは、シミュレーションで並列タスクの同時性と FIFO チャネルを介したこれらタスクの相互的なやり取りを確認できます。

Vitis HLS ツールでのデータ駆動型 TLP のインプリメンテーションでは、シンプルなクラスを使用してタスク ( hls::task ) およびチャネル ( hls::stream/hls::stream_of_blocks ) をモデル化します。

重要: Vitis HLS で最上位関数の hls::tasks はサポートされますが、最上位関数内のインターフェイスに hls::stream_of_blocks は使用できません。

次のシンプルなタスク チャネルの例について説明します。

#include "test.h"
 
void splitter(hls::stream<int> &in, hls::stream<int> &odds_buf, hls::stream<int> &evens_buf) {
    int data = in.read();
    if (data % 2 == 0)
        evens_buf.write(data);
    else
        odds_buf.write(data);
}
 
void odds(hls::stream<int> &in, hls::stream<int> &out) {
    out.write(in.read() + 1);
}
 
void evens(hls::stream<int> &in, hls::stream<int> &out) {
    out.write(in.read() + 2);
}
 
void odds_and_evens(hls::stream<int> &in, hls::stream<int> &out1, hls::stream<int> &out2) {
    hls_thread_local hls::stream<int> s1; // channel connecting t1 and t2      
    hls_thread_local hls::stream<int> s2; // channel connecting t1 and t3
 
    // t1 infinitely runs function splitter, with input in and outputs s1 and s2 
    hls_thread_local hls::task t1(splitter, in, s1, s2); 
    // t2 infinitely runs function odds, with input s1 and output out1
    hls_thread_local hls::task t2(odds, s1, out1); 
    // t3 infinitely runs function evens, with input s2 and output out2 
    hls_thread_local hls::task t3(evens, s2, out2); 
}

特殊な hls::task C++ クラスは次のとおりです。

  • ソース コードの新しいオブジェクト宣言で、特殊な修飾子を必要とするもの。hls_thread_local 修飾子は、複数回のインスタンシエーション関数 (この例では odds_and_evens) の呼び出し間で、オブジェクト (およびその下位スレッド) をアクティブに維持するために必要です。

    hls_thread_local 修飾子は、データ駆動型 TLP モデルの C シミュレーション動作が RTL シミュレーション動作と同じであることを確認するためにのみ必要です。RTL では、これらの関数は一度開始されると常時実行モードになります。C シミュレーションで同じ動作となるように、hls_thread_local 修飾子を使用して、各タスクが一度だけ開始し、複数回呼び出されても同じステートを維持することを確認する必要があります。hls_thread_local 修飾子が使用されない場合、関数を呼び出すたびに新しいステートになります。

  • タスク オブジェクトは関数を無限に実行するスレッドを暗示的に管理し、引数セットを渡します。引数は hls::stream または hls::stream_of_blocks のいずれかです。
    ヒント: これ以外の種類の引数はサポートされていません。定数値であっても関数引数として渡すことはできません。定数をタスク本体に渡す必要がある場合は、関数をテンプレート化した関数として定義し、このテンプレート化した関数に定数をテンプレート引数として渡します。
  • 渡された関数 (上記の例ではsplitter/odds/evens) はタスク本体と呼ばれ、暗示的に無限ループで囲まれているため、タスクは継続して実行されて入力を待機します。
  • 提供された関数にはパイプライン処理されたループを含めることができますが、これらのループはデッドロックを回避するためにフラッシュ可能パイプライン (FLP) とする必要があります。ツールは、任意のパイプライン処理されたループまたは関数に使用する適切なパイプライン形式を自動的に選択します。
重要: hls:task は関数呼び出しとして処理すべきではありません。hls::task はチャネルに静的に割り当てられた永続的なインスタンスとして処理する必要があります。このため、hls::tasks を含む関数の複数の呼び出しを 1 つにまとめるか、これらの呼び出しが同じ hls::tasks およびチャネルを使用するようにする必要があります。

チャネルは、テンプレート化された特殊な hls::stream (または hls::stream_of_blocks) C++ クラスによってモデル化されています。これらのチャネルには次の属性があります。

  • データ駆動型 TLP モデルでは、hls::stream<type,depth> オブジェクトは指定された深さの FIFO のように動作します。このようなストリームのデフォルトの深さは 2 で、ユーザーはこれを変更できます。
  • ストリームは、シーケンシャルに読み出されて書き込まれます。つまりデータ アイテムが hls::stream<> から読み出されると、それが再び読み出されることはありません。
    ヒント: 異なるストリームへのアクセスは順序付けられていません。たとえば、ストリームへの書き込み順序と別のストリームからの読み出し順序は、スケジューラによって変更できます。
  • ストリームは、ローカルまたはグローバルのいずれかに定義できます。グローバル スコープで定義されるストリームは、その他のグローバル変数と同じ規則に従います。
  • hls_thread_local 修飾子は、ストリーム (次のコード例では s1 および s2) にも必要です。これにより、複数回のインスタンシエーション関数 (次のコード例では odds_and_evens) の呼び出し間で同じストリームがアクティブに維持されます。

次に、上記のコード例を Vitis HLS で図式化したものを示します。この図では、緑色の矢印は FIFO チャネルで、青色の矢印はインスタンシエーション関数 (odds_and_evens) の入出力を示しています。タスクは青色の四角いボックスで示しています。

図 1. hls::task 例のデータフロー図

空ストリームの読み出しはブロッキング読み出しであるため、次の理由でデッドロックが発生することがあります。

  • デザインそのもの。プロセスごとに生産と消費のレートが不均衡。
    • C シミュレーションでは、空チャネルを読み出そうとする、1 サイクルのプロセスまたは最上位入力から駆動される一連のプロセスによってのみデッドロックが発生する可能性があります。
    • デッドロックは、C/RTL の協調シミュレーション実行時およびハードウェア実行時に、フル チャネルへの書き込みや空チャネルからの読み出しを試みるプロセス サイクルによって発生する可能性があります。
  • テストベンチ。計算結果を確認する際に、すべての出力を生成するために必要なデータよりも少ないデータが提供される。

このため、デザインに hls::task が含まれる場合、デッドロック検出が自動的にインスタンシエートされます。デッドロック検出によりデッドロックが検出され、C シミュレーションが停止します。gdb などの C デバッガーを使用するとさらにデバッグが実行され、空チャネルから読み出そうとするシミュレーションされた hls::tasks がすべてブロックされるところを確認します。デッドロックのデバッグは、handling_deadlock 例で示すように、Vitis HLS GUI を使用して簡単に実行できます。

hls::task モデルは、完全にデータ駆動型で純粋なストリーミング タイプの動作が必要なデザインで、ある種の制御がない場合に推奨されます。このようなモデルは、フィードバックおよび動的なマルチレート デザインのモデリングにも便利です。デザインのフィードバックは、タスクの間にサイクリック依存がある場合に発生します。プロデューサーがデータを書き込むレート、またはコンシューマーがデータを読み出すレートがデータに依存する動的マルチレート モデルは、データ駆動型 TLP によってのみ処理できます。この例は、GitHub の simple_data_driven デザインを参照してください。

ヒント: データに依存しないレートでプロデューサーがデータを書き込み、コンシューマーがデータを読み出す静的マルチレート デザインは、データ駆動型と制御駆動型の両方の TLP で管理できます。たとえば、プロデューサーは呼び出しごとに 2 つの値をストリームに書き込み、コンシューマーは呼び出しごとに 1 つの値を読み出します。