VLIW と MN-Core

MN-Core プログラミングを試していると、よくわからない制約やアセンブルエラーに遭遇します。

例えば、

iadd $lr0v $lr4v $lr8v

このような単純な命令とメモリの組み合わせでも

Instruction field conflict detected between `in_grf0` and `in_grf0`. Line 1 iadd $lr0v $lr4v $lr8v

このようなエラーが発生します。このエラーは使うメモリポートの組み合わせの制限から来ています。

一般的なプロセッサでは、ポート数は命令で使用する最大量を供給できるように設計されているか、命令が要求するメモリ(レジスタ)アクセスがポート数を超えると自動でストール(待機)するようにして、プログラマからは見えないように設計されています。

しかし、ポート数は理論ピーク演算性能に影響を与えないにもかかわらず、回路規模を大きくする要因になることが知られています。

MN-Core 2 では回路規模あたりの演算性能を最大化させるために、メモリポートの数を LM0, LM1, GRF0, GRF1, T の \(5\) つに減らしています。

ポートが衝突した場合、一般的なプロセッサではストールして性能が落ちるなどの形で表面化しますが、MN-Core では「アセンブルに失敗する」という形で表面化します。これについて詳しく解説します。

一般的なプロセッサの命令では、

・ALU を使用し、r8 へ r9 を加算

のように、個々の演算命令フォーマット中に、特定のレジスタへのアクセスが埋めこまれています。

MN-Core では「演算指示を出す」機械語と「メモリアクセスユニットへ指示を出す」機械語が命令中に専用領域として独立しています。

どういう事かというと、例えば、

iadd $r255 $lm4094 $r85 # GRF0の255番と、LM0の4094番を足して、GRF0の85番に書き込む

このアセンブリは、

・GRF0 は、\(255\) 番目のメモリを読んで値を出す

・LM0 は、\(4094\) 番目のメモリを読んで値を出す

・ALU は、GRF0 が出した値と LM0 が出した値を足して、結果を出す

・GRF0 は、ALU が出した値を \(85\) 番目のメモリに書き込む

という機械語に変換されます。

実際に細かく見てみましょう。アセンブラで

./assemble3 --instruction-mode flat source.vsm

とアセンブルしてみると、以下のような出力が得られます。

j 010000000000000000000000010100101010100000111111110000000000000100000000000000111111111110000001000000000000000000000000000000000000000011111111101000000000000011000000000000000000000000000000000111000000000000000000000000000000001100000000000000001010101011111111000000000000000000111111111110000000000000001010101011111111000000000000000000111111111110000000000000001010101011111111000000000000000000111111111110000000000000

j から始まるバイナリは Flat モードの PE 命令を表しています。$lm[0,2,10,14] のような自由なアドレスを使用可能にする Flat モードでは \(1\) PE 命令が \(416\) bit になります。

これは iadd で使用する ALU への指示だけではなく、MAU の演算・行列レジスタの指示, L1B, L2B などへの指示も含まれています。

$lm0v のような \(4\) サイクルのアドレスを増分だけで指定する Auto Stride モードを使用すると、\(1\) PE 命令が \(292\) bit になります。./assemble3 source.vsm とアセンブルすると、Auto Stride モードでアセンブルされます。

i 0100000000000000000000000101001010101000000000001111111100000000000000000000000000000001000000000000000000000011111111111000000000000000001000000000000000000000000000000000000000000000000000111111111010000000000000110000000000000000000000000111000000000000000000000000000000001100000000000000

i から始まるバイナリは Auto Stride モードの PE 命令を表しています。

MN-Core 2 の Auto Stride モードでは \(3\) つの PE 命令と \(1\) つの MV 命令を、Flat モードでは \(2\) つの PE 命令と \(1\) つの MV 命令を、\(1024\) bit にパックして、命令ストリームに送信します。

さて、Auto Stride モードのバイナリを例に、機械語の意味を見ていきましょう。アセンブラに ./assemble3 --print-csv source.vsm と、--print-csv オプションを渡すと変換された機械語の各ビットの意味が書かれた CSV で機械語ビット列を出してくれます。

かいつまんで順番に説明していきましょう。

まず、GRF0 への指示です。GRF0 へ指示するのは、次の二点です。

・\(255\) 番目のメモリを読んで値を出す

・ALU が出した値を 85 番目のメモリに書き込む

対応するビット列は以下の部分です。

pe.rfc0.radr 011111111 GRF0 は 255 (0b011111111) 番目のメモリを読む pe.rfc0.rwl 00 読み込むワード長。00:単語、01:長語、10:二長語 pe.rfc0.write 1 GRF0 に値を書く場合は 1 pe.rfc0.isel 01 書き込む値のセレクタ。[MAU-calc, ALU, L1BM, MAU-mread] の 4 択 pe.rfc0.wadr 001010101 GRF0 の 85 (0b001010101) 番目のメモリに書き込む pe.rfc0.wwl 00 書き込むワード長。00:単語、01:長語、10:二長語

これを解釈すると、

・GRF0 の読み込みでは \(255\) 番目のメモリを読み込む

・GRF0 の書き込みでは、ALU が出した値を \(85\) 番目のメモリに書き込む

となることがわかります。

次に LM0 です。対応するビット列は以下の部分です。

pe.lm0.write 0 書く場合は 1 pe.lm0.isel 00 書く場合の入力セレクタ。今回読み込みだけなので無視 pe.lm0.adr 111111111110 読み書きするメモリの番号。今回は4094 なので 0b111111111110 pe.lm0.wl 01 読み書きのワード長。00:単語、01:長語、10:二長語

これは

・LM0 は \(4094\) 番目のメモリを読んで値を出す

と解釈できます。LM は \(2\) つのうち片方を連続で読み、片方は連続で書き込む排他的使用を想定しているため、アドレスは入出力で共通のものを使用します。

最後にALUです。対応するビット列は以下の部分です。

pe.aluc.dmode 01 演算長。00:長語(long)、01:単語(int)、10:半語(short) pe.aluc.aluop 00000 ALUで行う演算、今回は加算なので 00000 pe.aluc.isela 0000 1つ目の入力のソース。GRF から取ってくるときは 0000 pe.aluc.iselb 011 2つ目の入力のソース。LM0 から取ってくるときは 011

これを解釈すると、

・ALU は、GRF が出した値と LM0 が出した値を Int として加算して、結果を出す

となります。

このように、MN-Core の機械語は、演算器への指示と、メモリユニットへの指示が明確に分かれた機械語になっています。

また、MN-Core では並列実行条件を満たす限り、ALU や MAU などの複数の演算器を同時に動かすことができます。

これは MN-Core の \(1\) 命令には複数の演算器の指示がそれぞれ独立した専用領域として含まれているためです。

このような \(1\) 命令で複数の演算器を同時に動かすことができるアーキテクチャを、VLIW(Very Long Instruction Word)と呼びます。

以下の図は、PE の中の構造を簡略化して表現したものです。

図の配置の都合上、左から順に「演算結果の書き込みセレクタ」「メモリ」「演算器への入力セレクタ」「演算器」となっています。

図は簡略化しているため、実際の PE の構造とは異なる部分があります。例えば、ALU 以外のフォーワーディングパス ($mauf, $mreadf, $lbf) は省略されていますし、ALU の入力のセレクタ (MUX) は本当は A 入力と B 入力でそれぞれ別物として独立しており、A 入力は PE 番号などの固定値入力が選択可能です。また ALU は PE 間循環シフト命令や BF 化命令のために、隣の PE の ALU との通信経路が備わっています。

このように MN-Core では少ないポート数にもかかわらず、ALU や MAU、L1B などを制約を満たす限り同時に実行することが可能であり、計算資源を最大限活用することができます。

また各ユニットへの指示が細かく完全に独立しているため命令デコーダも必要なくなり、回路規模を抑えることができます。

これを理解すれば、

iadd $lr0v $lr4v $lr8v

という命令がアセンブルに失敗するのも自然と理解できるはずです。GRF0 に、$lr0v$lr4v を同時に読むように指示する方法はなく、対応する機械語は存在しないからです。

それに対して、

iadd $lr0v $lr0v $lr8v

はアセンブルに成功します。この場合、GRF0 は $lr0v を読み、ALUの入力セレクト (isela, iselb) がどちらも GRF0 になります。

また、以下のように \(1\) つの演算器の出力を複数のメモリに書き込むことも MN-Core では可能です。

linc $lr0v $lr8v $ls20v $n314 $llm20 $t $omr1

インクリメント命令 linc は \(1\) 引数の命令なので、残りの \(6\) つは出力です。

一般的なプロセッサに慣れているとびっくりしてしまいますが、仕組みを考えれば納得がいくことでしょう。それぞれのメモリユニットの書き込み元を、ALU の出力にするだけだからです。