Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Section 2e - マルチコアプログラミング

このセクションでは、単一コア設計をマルチコア設計に変換する方法を実演します。高レベルIRONと明示的配置レベルIRONの両方について説明します。

高レベルIRON

初期設定とデータ移動

最初のシンプルな設計(aie2.py)では、<48xi32>データ型のオブジェクトを持つ4つのObject FIFOを使用します:外部メモリとメモリタイル間の入力用に1つ(of_in)、メモリタイルとWorker間の入力用に1つ(of_in1)、Worker実行出力用に1つ(of_out1)、出力をメモリタイルから外部メモリに戻すために1つ(of_out)です。Workerのタスクは、入力を取得し、エントリを1ずつ増加させ、その結果を出力に格納し、両方を解放することです。

単一Worker設計のデータ移動:

data_size = 48

# テンソル型の定義
data_ty = np.ndarray[(data_size,), np.dtype[np.int32]]

# 入力データ移動
of_in = ObjectFifo(data_ty, name="in")
of_in1 = of_in.cons().forward(obj_type=data_ty, name="in1")

# 出力データ移動
of_out1 = ObjectFifo(data_ty, name="out1")
of_out = of_out1.cons().forward(obj_type=data_ty, name="out")

スケールアウトした設計(aie2_multi.py)では、単一のメモリタイルを維持しますが、3つのWorkerに拡張します。Object FIFOオブジェクトのデータ型は<16xi32>に変更され、複数のWorker間でデータを分散および結合するためのObject FIFOリンク(分散と結合パターンを参照)が追加されました:

マルチWorker設計のデータ移動:

n_workers = 3
data_size = 48
tile_size = data_size // 3

# テンソル型の定義
data_ty = np.ndarray[(data_size,), np.dtype[np.int32]]
tile_ty = np.ndarray[(tile_size,), np.dtype[np.int32]]

# 入力データ移動
of_offsets = [tile_size * worker for worker in range(n_workers)]

of_in = ObjectFifo(data_ty, name="in")
of_ins = (
    of_in
    .cons()
    .split(
        of_offsets,
        obj_types=[tile_ty] * n_workers,
        names=[f"in{worker}" for worker in range(n_workers)],
    )
)

# 出力データ移動
of_out = ObjectFifo(data_ty, name="out")
of_outs = (
    of_out.prod().join(
        of_offsets,
        obj_types=[tile_ty] * n_workers,
        names=[f"out{worker}" for worker in range(n_workers)],
    )
)

Worker実装

最初のWorkerの実装では、配列演算が可能なPythonスクリプトを参照するだけです。

単一Worker:

# コアが実行するタスク
def core_fn(of_in, of_out):
    elem_in = of_in.acquire(1)
    elem_out = of_out.acquire(1)
    for i in range_(data_size):
        elem_out[i] = elem_in[i] + 1
    of_in.release(1)
    of_out.release(1)


# タスクを実行するWorkerを作成
my_worker = Worker(core_fn, [of_in1.cons(), of_out1.prod()])

マルチWorker設計では、同じカーネルが異なるオブジェクトサイズで3回インスタンス化されます。

複数のWorker:

# タスクを実行するWorkerを作成
workers = []
for worker in range(n_workers):
    workers.append(
        Worker(
            core_fn,
            [
                of_ins[worker].cons(),
                of_outs[worker].prod(),
            ],
        )
    )

ランタイムシーケンス

最後に、ランタイムシーケンスは、AIE配列との間でデータを移動させるホスト実行コードを定義します。

単一Workerランタイム:

# AIE配列との間でデータを移動するランタイム操作
rt = Runtime()
with rt.sequence(data_size, data_size, data_size) as (a_in, b_out, _):
    rt.start(my_worker)
    rt.fill(of_in.prod(), a_in)
    rt.drain(of_out.cons(), b_out, wait=True)

マルチWorker設計では、rt.start()への呼び出しが少し変わるだけです。

マルチWorkerランタイム:

# AIE配列との間でデータを移動するランタイム操作
rt = Runtime()
with rt.sequence(data_size, data_size, data_size) as (a_in, b_out, _):
    rt.start(*workers)
    rt.fill(of_in.prod(), a_in)
    rt.drain(of_out.cons(), b_out, wait=True)

コンパイル

両方の設計を次のコマンドでコンパイルして実行できます:

make all

明示的配置レベルIRON

タイル宣言

最初の設計(aie2_placed.py)では、3つのタイルを宣言します:1つのコンピュートタイル、1つのメモリタイル、1つのshimタイルです。これらのタイルのリソースは、それぞれデータ処理、ストレージ、外部メモリアクセスの目的で割り当てられます。

単一コア設計のタイル:

ShimTile = tile(0, 0)
MemTile = tile(0, 1)
ComputeTile = tile(0, 2)

マルチコア設計(aie2_placed_multi.py)では、1つのコンピュートタイルの代わりに3つのコンピュートタイルを宣言します。

マルチコア設計のタイル:

n_cores = 3

ShimTile = tile(0, 0)
MemTile = tile(0, 1)
ComputeTiles = [tile(0, 2 + i) for i in range(n_cores)]

データ移動の設定

次に、タイル間のデータ移動を設定します。高レベルIRONでのアプローチと同様に、最初の設計では4つのObject FIFOを使用し、マルチコア設計ではObject FIFOリンクを使用します。

単一コア設計のObject FIFO:

data_size = 48
buffer_depth = 2
data_ty = np.ndarray[(48,), np.dtype[np.int32]]


# 入力データ移動

of_in = object_fifo("in", ShimTile, MemTile, buffer_depth, data_ty)
of_in0 = object_fifo("in0", MemTile, ComputeTile, buffer_depth, data_ty)
object_fifo_link(of_in, of_in0)


# 出力データ移動

of_out = object_fifo("out", MemTile, ShimTile, buffer_depth, data_ty)
of_out0 = object_fifo("out0", ComputeTile, MemTile, buffer_depth, data_ty)
object_fifo_link(of_out0, of_out)

マルチコア設計のObject FIFOは、最初の設計から大きく変更されています(分散と結合パターンを参照)。

マルチコア設計のObject FIFO:

n_cores = 3
data_size = 48
tile_size = data_size // 3

buffer_depth = 2
data_ty = np.ndarray[(data_size,), np.dtype[np.int32]]
tile_ty = np.ndarray[(tile_size,), np.dtype[np.int32]]

# 入力データ移動
inX_fifos = []

of_in = object_fifo("in", ShimTile, MemTile, buffer_depth, data_ty)
for i in range(n_cores):
    inX_fifos.append(object_fifo(
        f"in{i}", MemTile, ComputeTiles[i], buffer_depth, tile_ty
    ))

# 入力/出力データの結合/分散のためのオフセットを計算
if n_cores > 1:
    of_offsets = [16 * i for i in range(n_cores)]
else:
    of_offsets = []
object_fifo_link(of_in, inX_fifos, [], of_offsets)


# 出力データ移動
outX_fifos = []

of_out = object_fifo("out", MemTile, ShimTile, buffer_depth, data_ty)
for i in range(n_cores):
    outX_fifos.append(object_fifo(
        f"out{i}", ComputeTiles[i], MemTile, buffer_depth, tile_ty
    ))
object_fifo_link(outX_fifos, of_out, of_offsets, [])

コア実装

この段階では、各コンピュートタイルがどのコードを実行するかを指定します。

単一コア:

@core(ComputeTile)
def core_body():
    # 実質的にwhile(1)
    for _ in range_(0xFFFFFFFF):
        elem_in = of_in0.acquire(ObjectFifoPort.Consume, 1)
        elem_out = of_out0.acquire(ObjectFifoPort.Produce, 1)
        for i in range_(data_size):
            elem_out[i] = elem_in[i] + 1
        of_in0.release(ObjectFifoPort.Consume, 1)
        of_out0.release(ObjectFifoPort.Produce, 1)

マルチコア設計では、各コアは前のステップのObject FIFO設定に応じて、適切なObject FIFOのインデックスを使用します。重要な違いは、data_sizeではなくtile_sizeを処理することです。

マルチコア:

for i in range(n_cores):
    # コンピュートタイル i
    @core(ComputeTiles[i])
    def core_body():
        for _ in range_(0xFFFFFFFF):
            elem_in = inX_fifos[i].acquire(ObjectFifoPort.Consume, 1)
            elem_out = outX_fifos[i].acquire(ObjectFifoPort.Produce, 1)
            for j in range_(tile_size):
                elem_out[j] = elem_in[j] + 1
            inX_fifos[i].release(ObjectFifoPort.Consume, 1)
            outX_fifos[i].release(ObjectFifoPort.Produce, 1)

コンパイル

両方の設計を次のコマンドでコンパイルして実行できます:

make placed

注意: より詳細な情報と完全なコード例については、公式ドキュメントを参照してください。