FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

NVDLAのConvolution DMAが実行する畳み込みの手順の解析

NVDLAというか、畳み込み演算をどのようにハードウェアで実現するかということをさらに掘り下げている。

  • NVDLA : Unit Description

Unit Description — NVDLA Documentation

NVDLAのConvolution DMAは、以下のような画像に対して入力画像とカーネルを畳み込むことを考える。ここではチャネルについては無視している。

上記の図における、各パラメータは以下の通りである。

  • Top Padding(TP) : 画像データに対して上部に何ピクセルパディングを入れるか。
  • Bottom Padding(BP) : 画像データに対して下部に何ピクセルパディングを入れるか。
  • Left Padding(LP) : 画像データに対して左部に何ピクセルパディングを入れるか。
  • Right Padding(RP) : 画像データに対して右部に何ピクセルパディングを入れるか。
f:id:msyksphinz:20180917161117p:plain
図. 画像畳み込みの前提条件パラメータ

これに対して、カーネルの畳み込みを行うと以下のようになる。ここでSXSYは畳み込みのストライド数である。 それぞれSX=4SY=3として表現している。

W', H' というのは畳み込み後の画像のサイズであるが、これを以下のように表現している。

  • W′=LP+W+RP−S′SX+1
  • H′=TP+H+BP−R′SY+1

ただし、

  • S′=(S−1)\times DX+1
  • R′=(R−1)\times DY+1

畳み込みをしていくのは以下のようなイメージだ。ここではX方向のストライド数=4, Y方向のストライド数=3としている。

f:id:msyksphinz:20180917161941p:plain
図. 画像に対する畳み込みの適用。

NVDLAにおける畳み込み演算

NVDLAにおけるアトミック操作

アトミック操作は重みカーネルの1ドットに対して掛け算を実施する計算に相当する。

f:id:msyksphinz:20180917163050p:plain
図. カーネルK0 - K15 に対してドット0を掛け合わせる。

これは以下の計算をしていることに相当する。

  • [0] × [00](K0)
  • [0] × [00](K1)
  • [0] × [00](K2)
  • [0] × [00](K3)
  • [0] × [00](K15)

ここでは、カーネルの数を1として考えてみる。そうすると、上記の図における画像の座標0と、カーネルの座標00の乗算を行っていることに相当する。

f:id:msyksphinz:20180917164000p:plain

NVDLAにおけるストライプ操作

ストライプ操作は、上記のアトミック操作を連続して実行することに相当する。カーネルの位置は変えずに、入力画像の位置をずらす。

先ほどの式を拡張して考えると、上記の図における画像の座標0から座標21までの四角形の16要素と、カーネルの座標00の乗算を行っていることに相当する。

f:id:msyksphinz:20180917164204p:plain

NVDLAにおけるブロック操作

ブロック操作は、上記のブロック操作をさらに連続して実行する。画像の位置と、カーネルの位置を変えながら演算を行う。カーネルのサイズは3\times 3なので、画像の位置を0,1, 2, 6, 7, 8, 12, 13, 14とずらす。 一方でカーネルの位置も、00, 01, 02, ... 08 までずらす。

先ほどの式を拡張して考えると以下のような演算を実行している。

f:id:msyksphinz:20180917164548p:plain

最終的な畳み込みは以下のように行方向に加算することで計算できる。

f:id:msyksphinz:20180917165553p:plain

NVDLAの内部構造調査(10. NVDLAのConvolution DMA)

NVDLAの内部構造についてもう少し詳しく解析したいのだが、割としっかりと解説してあるページがあったので読み進めていこう。

参考ににしたのは以下。っていうかNVDLA本家のページである。 今回はConvolutionのためのモジュール。Convolution DMA(CDMA)である。

  • Unit Description (NVDLA)

Unit Description — NVDLA Documentation

畳み込みDMA

概要

畳み込みDMA(CDMA)は畳み込みパイプラインを実行するパイプラインステージである。SRAM/DRAMからデータをフェッチしデータを畳み込みエンジンの処理できるようにバッファ(Convolution Buffer: CBUF)に格納する。サポートしている入力フォーマットは:

  • ピクセルデータ
  • 特徴データ
  • 圧縮・非圧縮の重みデータ
  • WMB
  • WGS

CDMAからAXIに2つのチャネルが接続されている。重みデータの読み込みチャネルと、データ読み込みのためのチャネルである。上記の入力フォーマットをフェッチするために、チャネルは上記のフォーマットのために構成してある。以下の表には、読み込みチャネルにマッピングするための入力データフォーマットを示してある。

入力フォーマット Image Case Uncompressed Feature Case Uncompressed Weight Case Compressed Weight Case
Pixel data data channel NA NA NA
Uncompressed feature data NA data channel NA NA
Uncompressed weight NA NA weight channel NA
Sparse compressed weight NA NA NA weight channel
WMB NA NA NA weight channel
WGS NA NA NA weight channel

畳み込みDMAはメモリの読み込みリクエストしか発行しない。すべてのメモリリクエストは64-byteのアドレスにアラインされて発行される。

../../../_images/ias_image17_cdma.png

図 53 畳み込みDMA

CDMAは3つのサブモジュールから構成されており、ピクセルデータと特徴データをフェッチする: CDMA_DC, CDMA_WG, CDMA_IMGである。これらのサブモジュールの役割は似通っているが、データをCBUF RAMへ書き込むか順番が異なる。 どのような場合にも、サブモジュールのうちの1つがピクセル・特徴データをフェッチするためにアクティベートされる。

ここでは、CDMA_DCを例として取り上げる:

  • Convolution Bufferの空き領域をチェックする。
  • 読み込みトランザクションを生成する。
  • フェッチデータを共有バッファにキャッシュする。
  • 特徴キューブを正しい順番に整形する。
  • Convolution Bufferの書き込みアドレスを生成する。
  • CDMA_STATUSサブモジュール内のConvolution Bufferの状態をアップデートする。

Convolution DMAはWinogradの処理を実行するために所望のエンジンを使用する。CDMA_WGはCDMA_DCと非常に似通った構造をしているが、Convolution Buffer内の最終的な特徴データの構造は異なっている。したがってCDMA_WGは特殊なフェッチシーケンスを持っている。さらに、CDMA_WGはWinogradチャネル拡張を事項している。

CDMA_IMGエンジンはピクセルデータを外部メモリからフェッチする。データフォーマットに応じてアドレスを生成し、ピクセルの要素の順番を変更してConvolution Bufferの適切なエントリに書き込む。CDMA_IMGの動作はCDMA_DCに似ているが、ピクセルデータを扱うというところが異なる。

CDMA_DCエンジンのみがマルチバッチモードをサポートしている。したがって、1つ以上の入力特徴データキューブを1つのHWレイヤでフェッチすることができ、性能を向上させることができる。最大バッチサイズは32である。

CDMAは重みのフェッチにも所望のエンジンを使用する: CDMA_WTである。CDMA_WTは他のDMAエンジンと比較するとシンプルであるが、同時に3つのRead steamをサポートしているところが異なる。もし入力重みフォーマットが圧縮されていないと、重みデータのみをフェッチする。もし重みフォーマットが圧縮されていると、WMBとWSGはすべてフェッチされる。重みフォーマットについては、Data Formats の項目を参照すること。

入力重みデータが圧縮されていると、2つのアービタが有効差化されてRead streamが読み込まれる。最初の重みラウンドロビンアービタが重みストリームもしくはWMBストリームのリクエストを受け付ける。次にそのアービトレーションの勝者がWGSの読み込みストリームと静的なアービトレーションを行う。WGSは常に高い優先度を持っている。最終的に勝利したリクエストが重みチャネルにデータフェッチリクエストを発行する。

CDMA_WTは可能な限り重みのフェッチが完了するか、Convolution Bufferのエントリが空いている限りConvolution Bufferを埋めようとする。

CDMAはCBUF中の重みバッファと入力データバッファの通信状態を管理している。CDMACSCには2つの状態コピーが存在している。データ要素が解放されたときに、2つのモジュールがアップデートとリリース情報を交換し、新しい特徴データ・ピクセルデータ・重みデータをいつフェッチするのかを決めている。

電力について

Convolution DMAはデータパス上にクロックゲーティングが実施されている。Convolution DMAのデータパスのクロックは、データが存在しないか、プログラムレジスタにおいてハードウェアレイヤが構成されていないときにゲーティングさえっる。CDMAレジスタファイルのサブモジュールはクロックゲーティングされないため、新しいコマンドは常に受け付けることができる。

Convolution Buffer

概要

Convolution Buffer(CBUF)は畳み込みパイプライン中のステージである。512KBのSRAMで構成される。SRAMCDMAモジュールからの入力ピクセルデータ・特徴データ・重みデータとWMBデータを読み取る。これにはConvolution Sequence Generatorモジュールを使用する。CBUFは2つの書き込みポートと3つの読み込みポートを持っている。

CBUFは32KBのメモリが16バンク用意されている。各バンクは512-bit幅であり、256エントリの2ポートのSRAMで構成されている。これらのバンクは3つの論理Circularバッファとして動作する。

  • Input data buffer
  • Weight buffer
  • WMB buffer
  • 入力データバッファ
  • 重みバッファ
  • WMBバッファ

重みフォーマットが圧縮されているとき、バンク15はWMBバッファとして割り当てられ、残りの2種類のバッファはバンク0~バンク14に格納される。重みフォーマットが圧縮されていないとき、WMBバッファはどのバンクにも割り当てられず、データバッファと重みバッファは16バンクのすべてを使用する。必要なバンク数が16よりも小さい場合は、残ったバンクは使用されない。

各バッファはCircularバッファとして使用される。新しい入力データ・重み・WMBはエントリアドレスを持っており、常に上位方向に加算される。もしアドレスが最大値に到達すると、0にラッピングされて0から再度インクリメントされる。

../../../_images/ias_image18_cbuf.png

図54 Convolution Buffer

電力について

Convolution Bufferにはデータバス中のレジスタに対してクロックゲーティングが適用されている。Convolution Bufferのデータパスは、アイドル状態の場合か、プログラマブルレジスタによりどのHWレイヤも割り当てられていないとき、SLCGによりクロックゲーティングされる。Convolution Buffer内のコンフィグレーションレジスタはクロックゲーティングされず、したがって常にレジスタをプログラムすることができる。

畳み込みシーケンスコントローラ

概要

畳み込みシーケンスコントローラ(CSC)は入力特徴データ、ピクセルデータ、重みデータをCBUFからConvolution MACユニットに転送する役割を持つ。畳み込みパイプライン部において、畳み込みシーケンスの計算及び制御を行うためのカギとなるモジュールである。

畳み込みシーケンスコントローラ(CSC)3つのサブモジュールから構成されている: CSC_SG, CSC_WL, CSC_DLである。図55を参照のこと。

CSC_SGは畳み込みシーケンスジェネレータである。このモジュールは畳み込み操作の制御シーケンスを生成する。

CSC_SGのフローは以下の通りである。

  1. CBUF中に十分なデータと重みが格納されるまでポーリングする。
  2. シーケンスパッケージを生成する。シーケンスパッケージには、重みパッケージをロードするシーケンスと、データをロードするシーケンスが含まれる。各パッケージは1つのストライプ操作として表現される。
  3. 2つのパッケージをFIFOに挿入する。
  4. 重みと特徴・ピクセルのための2つのカウンタである。ダウンカウンタである。
  5. カウンタが0になると、Convolution Accumulatorからの信号をチェックしてバックプレッシャをチェックする。
  6. 全ての状態がReadyであれば、適切な時間に重みとデータパッケージをCSC_WLとCSC_DLに転送する。

../../../_images/ias_image19_csc.png

図55 畳み込みシーケンスコントローラ

CSC_DLは畳み込みデータローダである。このモジュールは特徴・ピクセルローディングのシーケンスを実行する論理が入っている。このモジュールはシーケンスジェネレータからパッケージを受け取り、特徴・ピクセルデータをCBUFからロードしてConvolution MACに転送する。データバッファの状態を管理し、CDMAとやり取り行って状態を常に最新に保つ。Winogradモードのために、入力特徴データを変換するためにPRA(Pre-addition)も行う役割がある。

CSC_WLはConvolution Weight Loaderの略である。このモジュールは重みをロードするシーケンサの論理が入っている。シーケンスジェネレータからパッケージを受け取り、CBUFから重み情報を受け取り、必要な回答処理を行ってからConvolution MACに転送する。重みバッファの状態を管理し、CDMA_WTと通信を行って状態を最新に保つ。

電力について

畳み込みシーケンスコントローラはデータパス中のレジスタに対してクロックゲーティングが適用される。畳み込みシーケンスコントローラのデータパス中のクロックは、アイドル中であるかプログラマブルレジスタによって有効化されていない場合、クロックゲーティングが適用される。畳み込みシーケンスコントローラのサブモジュールのレジスタファイルはクロックゲートされないため、常に最新の情報を入力できる。

次世代計算機講座 <入門編> ~ゲート式量子コンピュータとアニーリングマシン~ に参加した

09/15に早稲田大学で行われた次世代計算機講座に参加した。

量子コンピュータについは何冊か本を読んでわかっているつもりだったのだが、独学の部分がほとんどなのでいろんな講演を聞いてみたいと思い参加した。

最初の京都大学の先生の話は量子コンピュータの基礎なので、私もYoutubeとか資料を読みまくってなんとなく分かっていた。 それ以降の量子アニーリング専用マシン・デジタルアニーラ等々までは詳しくは知らなかったので初耳の話が多かった。

ちなみに京都大学の先生の講義には、CQ出版のインタフェースも参考文献として出ていたのでもう一度読み直してみよう。

以下は私が講義中にとったメモ:


量子コンピュータ入門:基本原理からプログラミング環境まで 京都大学 藤井先生

多くのパタンを重ね合わせるが、高い確率で答えを出すためにはもう少し工夫が必要で、波の干渉による強めアイ・弱めあいを使って答えを算出する必要がある。

量子ビットの実現方法

量子コンピュータの種類

量子ビットの操作

アダマール変換。量子ビットをリンギング(重ね合わせ状態)させる操作。 アダマール演算を2回実行すると単位行列なので基本的にはそのままになるはず。 1回アダマール行列を掛けることにより重ね合わせになって確率が分散するはずなのだが、そうでないのが量子計算。

2量子ビットの演算

CNOT : Conditional Not

量子ビットは数が増えると表現できる数の種類が指数関数状に増える → 量子コンピュータの強いところ。

万能量子コンピュータ

CNOT / H / T の演算が実現できれば、万能量子コンピュータが実現できる。

観測も確率で表現されるので、内部の行列を適切に使用して確実に答えが観測できるように工夫する必要がある。

最近の動向

NISQ : 50~100量子ビットで、ノイズがあるけれども中規模の量子コンピュータ

大規模化を行うためには、ノイズ対策が不可欠。

アニーリングマシンの研究開発現状と使い方入門 早稲田大学 田中先生

アニーリング問題→組み合わせ問題を高速に解きたい。

イジングモデル

イジングモデルは磁石の相転移を書き下すことができる。

組み合わせ最適化問題の最適解=イジングモデル基底状態(最も安定した状態)

詳しくは https://quantum.fixstars.com を見てね。

0-1整数計画問題で、2次の項まで落とせていればイジングマシンに変換できる。

数分割問題の場合、集合1に属するものを-1, 集合2に属するものを+1と考えるとイジングモデルに変換できる。 分割点を最小化する問題は$\dfrac{1-s_is_j}{2}$として表現できる。 2つの項を足し算することで、ハミルトニアンを構成することができる。

組み合わせ最適化問題はすべての項に対して乗算を行う必要があるが、D-Waveの量子ビットネットワークはすべての量子ビットは接続されていない。したがってグラフを変換する必要がある。

https://www.rco.recruit.co.jp/career/engineer/blog/46/

富士通 デジタルアニーラの話 東さん

  • デジタル回路を使って、量子アニーリング問題を解決する → デジタルアニーラ(DAU)
  • 1024量子ビット・ビット完全結合が特徴。
  • 特徴
    • 局所解から脱出しやすいようなオフセットを挿入できる機能がある
  • 活用例
    • 投資ポートフォリオ
    • 生産管理スケジューリング
    • デジタルアニーラで、医療における放射線量の計算を高速化する

CMOSアニーリングマシンの概要 山岡さん

  • ナチュラルコンピューティング。物理現象を問題に写像する。

  • スパースなイジングモデルを作成した。これでは問題を写像するのが難しい。

  • 特徴
  • アニーリングの手法
    • ランドスケープに基づいて最適解を求める
    • ランダムジャンプにより局所解に陥ることを防ぐ。
      • 疑似乱数列をチップ内に入れていき、スピンの値を壊す。
  • 最大カット問題を解かせる。
  • CMOSアニーリングに量子アニーリングの手法を取り入れられないか?(第3世代)。

RISC-Vシミュレータの実装をLambda関数を使って簡単化してみる

GitHub上に公開している自作RISC-Vシミュレータは、命令フェッチ、命令デコーダ、命令実行の部分が分けて実装してある。

命令デコーダは500種類程度ある命令を1つに特定し、その情報に基づいて命令を実行するのだが、その命令の実装はそれぞれの命令で分割してある。 これは各命令で命令の動作を細かく制御できるようにするためだ。

f:id:msyksphinz:20180916025750p:plain

github.com

void InstEnv::RISCV_INST_LUI (InstWord_t inst_hex)
{
  RegAddr_t rd_addr = ExtractRDField (inst_hex);
  DWord_t   imm   = ExtendSign (ExtractBitField (inst_hex, 31, 12), 19);
  DWord_t   res   = m_pe_thread->SExtXlen (imm << 12);

  m_pe_thread->WriteGReg (rd_addr, res);
}


void InstEnv::RISCV_INST_AUIPC (InstWord_t inst_hex)
{
  RegAddr_t rd_addr = ExtractRDField (inst_hex);
  DWord_t   imm     = ExtendSign (ExtractBitField (inst_hex, 31, 12), 19);
  DWord_t   mask    = ~0xfff;
  UDWord_t  res   = ((imm << 12) & mask) + m_pe_thread->GetPC ();

  m_pe_thread->WriteGReg<UDWord_t> (rd_addr, m_pe_thread->SExtXlen(res));
}
...

こうすると問題は、この命令が実装してあるC++のファイルが増大しがちなのと、似たような命令に対して何度も同じような記述を繰り返さなければならないことだ。 例えば、add / sub / mul / divなどは演算子が異なるだけで、それ以外はすべて同じ実装となる。

これをすべて別々の命令実行関数として作るのはかなり骨が折れるし、ひとつ変えると全部の命令の仕様を変える必要があるため、大変だ。

そこで、整数命令(2R1W)、浮動小数点命令(2R1W)、浮動小数点(3R1W)などのテンプレートを用意して、それぞれの命令の細かい中身だけは分離して管理したい。 このため、各命令を記述した仕様書からコアの部分をラムダ式として記述し、記述を簡略化する方法を考えた。

命令の仕様

私の自作RISC-Vシミュレータの場合、命令の仕様は1つのRubyで記述された配列として記述してある。 命令の仕様書はCSV形式で書こうかと思ったが、記述の自由度とエディタで編集できることを考えると、スクリプト言語の一部として記述できた方がよいのかもしれない。

  • build/riscv_arch_table.rb
...
$arch_table.push(Array['add        r[11:7],r[19:15],r[24:20]'                                       , 32,  32,      '00000', '00',     'XXXXX', 'XXXXX', '000',    'XXXXX', '01100', '11', 'ALU',    "",           ["XS", "XS", "XS", "return m_pe_thread->SExtXlen(op1 +  op2)"]])
$arch_table.push(Array['sub        r[11:7],r[19:15],r[24:20]'                                       , 32,  32,      '01000', '00',     'XXXXX', 'XXXXX', '000',    'XXXXX', '01100', '11', 'ALU',    "",           ["XS", "XS", "XS", "return m_pe_thread->SExtXlen(op1 -  op2)"]])
...

命令デコードの仕様が最初に記述してあるが、必要なのは最後のこの部分だ。 この部分が、ラムダ関数で渡される。命令のテンプレートは、最初の3要素で決定される。

["XS", "XS", "XS", "return m_pe_thread->SExtXlen(op1 +  op2)"]
["XS", "XS", "XS", "return m_pe_thread->SExtXlen(op1 -  op2)"]

要素の1つ目がDestination Registerの型(この場合は64-bit Signed Registers)である。 また、2, 3番目の要素がSource Registerの型(64-bit Signed Registers)であることを示している。

このことから、このラムダ式を渡す関数はFunc_R_RR()であると決まる。以下のFunc_R_RR()関数のfunc()の部分が、ラムダ式に代わる。

  • src/riscv_inst_template.cpp
// Destination R, Source RR
template <typename Dst_t, typename Src_t, typename Func>
void RiscvPeThread::Func_R_RR (InstWord_t inst_hex, Func func)
{
  RegAddr_t rs1_addr = ExtractR1Field (inst_hex);
  RegAddr_t rs2_addr = ExtractR2Field (inst_hex);
  RegAddr_t rd_addr  = ExtractRDField (inst_hex);

  Src_t rs1_val = ReadGReg<Src_t> (rs1_addr);
  Src_t rs2_val = ReadGReg<Src_t> (rs2_addr);

  UWord_t fflags_dummy;
  Dst_t res     = func(rs1_val, rs2_val, 0, &fflags_dummy);   // ここの部分が、各命令で異なる部分。

  WriteGReg<Dst_t> (rd_addr, res);
}

そして、上記のRubyの仕様書から以下の関数を自動生成する。

  • src/inst_riscv__ALU.cpp
void InstEnv::RISCV_INST_ADD(InstWord_t inst_hex)
{
  m_pe_thread->Func_R_RR<DWord_t, DWord_t> (inst_hex, [&](DWord_t op1, DWord_t op2, uint32_t round_mode, UWord_t *fflags) { return m_pe_thread->SExtXlen(op1 +  op2); });
}
void InstEnv::RISCV_INST_SUB(InstWord_t inst_hex)
{
  m_pe_thread->Func_R_RR<DWord_t, DWord_t> (inst_hex, [&](DWord_t op1, DWord_t op2, uint32_t round_mode, UWord_t *fflags) { return m_pe_thread->SExtXlen(op1 -  op2); });
}

このテンプレートを活用することができれば、多くの算術演算は、演算の種類以外は共通のため、実際に記述しなければならないC++のコードの量を節約できる。

例えば、浮動小数点の関数群を以下のように記述した。

  • build/riscv_arch_table.rb
Array['fmadd.s    f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::FloatMadd (op1, op2, op3, fflags)"]]
Array['fmsub.s    f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::FloatMsub (op1, op2, op3, fflags)"]]
Array['fnmsub.s   f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::FloatNeg(InstOps::FloatMsub (op1, op2, op3, fflags))"]]
Array['fnmadd.s   f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::FloatNeg(InstOps::FloatMadd (op1, op2, op3, fflags))"]]
Array['fadd.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatAdd (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags) | 0xffffffff00000000ULL"]]
Array['fsub.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatSub (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags) | 0xffffffff00000000ULL"]]
Array['fmul.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatMul (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags) | 0xffffffff00000000ULL"]]
Array['fdiv.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatDiv (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags) | 0xffffffff00000000ULL"]]
Array['fsqrt.s    f[11:7],f[19:15]'                   ...     ["D", "D", "return InstOps::FloatSqrt (op1, softfloat_round_near_even, fflags)"]]
Array['fsgnj.s    f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "DWord_t c_op1 = ConvertNaNBoxing(op1); DWord_t c_op2 = ConvertNaNBoxing(op2); return (c_op1 & 0x7FFFFFFFULL) | ( c_op2          & 0x80000000ULL) | 0xffffffff00000000ULL"]]
Array['fsgnjn.s   f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "DWord_t c_op1 = ConvertNaNBoxing(op1); DWord_t c_op2 = ConvertNaNBoxing(op2); return (c_op1 & 0x7FFFFFFFULL) | (~c_op2          & 0x80000000ULL) | 0xffffffff00000000ULL"]]
Array['fsgnjx.s   f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "DWord_t c_op1 = ConvertNaNBoxing(op1); DWord_t c_op2 = ConvertNaNBoxing(op2); return (c_op1 & 0x7FFFFFFFULL) | ((c_op1 ^ c_op2) & 0x80000000ULL) | 0xffffffff00000000ULL"]]
Array['fmin.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatMin (op1, op2, fflags)"]]
Array['fmax.s     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::FloatMax (op1, op2, fflags)"]]
Array['fcvt.w.s   r[11:7],f[19:15]'                   ...     ["RS", "D", "return InstOps::Convert_StoW  (op1, round_mode, fflags)"]]
Array['fcvt.wu.s  r[11:7],f[19:15]'                   ...     ["RS", "D", "return InstOps::Convert_StoWU (op1, round_mode, fflags)"]]
Array['fmv.x.w    r[11:7],f[19:15]'                   ...     ""])
Array['feq.s      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::FloatEq (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags)"]]
Array['flt.s      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::FloatLt (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags)"]]
Array['fle.s      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::FloatLe (ConvertNaNBoxing(op1), ConvertNaNBoxing(op2), softfloat_round_near_even, fflags)"]]
Array['fclass.s   f[11:7],f[19:15]'                   ...     ""])
Array['fcvt.s.w   f[11:7],r[19:15]'                   ...     ""])
Array['fcvt.s.wu  f[11:7],r[19:15]'                   ...     ""])
Array['fmv.w.x    f[11:7],r[19:15]'                   ...     ""])
Array['fld        f[11:7],r[19:15],h[31:20]'          ...     ""])
Array['fsd        f[24:20],h[31:25]|h[11:7](r[19:15])'...     ""])
Array['fmadd.d    f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::DoubleMadd (op1, op2, op3, fflags)"]]
Array['fmsub.d    f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::DoubleMsub (op1, op2, op3, fflags)"]]
Array['fnmsub.d   f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::DoubleNeg(InstOps::DoubleMsub (op1, op2, op3, fflags))"]]
Array['fnmadd.d   f[11:7],f[19:15],f[24:20],f[31:27]' ...     ["D", "D", "D", "D", "return InstOps::DoubleNeg(InstOps::DoubleMadd (op1, op2, op3, fflags))"]]
Array['fadd.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleAdd (op1, op2, softfloat_round_near_even, fflags)"]]
Array['fsub.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleSub (op1, op2, softfloat_round_near_even, fflags)"]]
Array['fmul.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleMul (op1, op2, softfloat_round_near_even, fflags)"]]
Array['fdiv.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleDiv (op1, op2, softfloat_round_near_even, fflags)"]]
Array['fsqrt.d    f[11:7],f[19:15]'                   ...     ["D", "D", "return InstOps::DoubleSqrt (op1, softfloat_round_near_even, fflags)"]]
Array['fsgnj.d    f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return (op1 & 0x7FFFFFFFFFFFFFFFULL) | ( op2        & 0x8000000000000000ULL)"]]
Array['fsgnjn.d   f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return (op1 & 0x7FFFFFFFFFFFFFFFULL) | (~op2        & 0x8000000000000000ULL)"]]
Array['fsgnjx.d   f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return (op1 & 0x7FFFFFFFFFFFFFFFULL) | ((op1 ^ op2) & 0x8000000000000000ULL)"]]
Array['fmin.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleMin (op1, op2, fflags)"]]
Array['fmax.d     f[11:7],f[19:15],f[24:20]'          ...     ["D", "D", "D", "return InstOps::DoubleMax (op1, op2, fflags)"]]
Array['fcvt.s.d   f[11:7],f[19:15]'                   ...     ""])
Array['fcvt.d.s   f[11:7],f[19:15]'                   ...     ""])
Array['feq.d      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::DoubleEq (op1, op2, softfloat_round_near_even, fflags)"]]
Array['flt.d      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::DoubleLt (op1, op2, softfloat_round_near_even, fflags)"]]
Array['fle.d      r[11:7],f[19:15],f[24:20]'          ...     ["XS", "D", "D", "return InstOps::DoubleLe (op1, op2, softfloat_round_near_even, fflags)"]]

これらから、以下の命令の実装を自動生成できるようにした。

  • src/inst_riscv__FPU.cpp
void InstEnv::RISCV_INST_FMADD_S(InstWord_t inst_hex)
{
  m_pe_thread->Func_F_FFF<UDWord_t> (inst_hex, [](UDWord_t op1, UDWord_t op2, UDWord_t op3, UWord_t *fflags) { return InstOps::FloatMadd (op1, op2, op3, fflags); });
}
void InstEnv::RISCV_INST_FMSUB_S(InstWord_t inst_hex)
{
  m_pe_thread->Func_F_FFF<UDWord_t> (inst_hex, [](UDWord_t op1, UDWord_t op2, UDWord_t op3, UWord_t *fflags) { return InstOps::FloatMsub (op1, op2, op3, fflags); });
}
...
void InstEnv::RISCV_INST_FLT_D(InstWord_t inst_hex)
{
  m_pe_thread->Func_R_FF<DWord_t, UDWord_t> (inst_hex, [](DWord_t op1, UDWord_t op2, uint32_t round_mode, UWord_t *fflags) { return InstOps::DoubleLt (op1, op2, softfloat_round_near_even, fflags); });
}
void InstEnv::RISCV_INST_FLE_D(InstWord_t inst_hex)
{
  m_pe_thread->Func_R_FF<DWord_t, UDWord_t> (inst_hex, [](DWord_t op1, UDWord_t op2, uint32_t round_mode, UWord_t *fflags) { return InstOps::DoubleLe (op1, op2, softfloat_round_near_even, fflags); });
}

これで、自分で実装しなければならない命令の種類はかなり減るはずだ。

NVDLAの内部構造調査(9. NVDLAの各ブロックで何をしているのか)

NVDLAの内部構造についてもう少し詳しく解析したいのだが、割としっかりと解説してあるページがあったので読み進めていこう。

参考ににしたのは以下。っていうかNVDLA本家のページである。

  • Unit Description (NVDLA)

Unit Description — NVDLA Documentation

ブリッジDMA

概要

入力画像と、処理後の画像は外部DRAMに格納されているが、外部DRAMのバンド幅とレイテンシはNVDLAのMACアレイの性能を最大限に発揮するのには不十分である。したがってNVDLAはセカンダリオンチップSRAMのインタフェースと一緒に構成されている。

オンチップSRAMを活用するためには、NVDLAはデータを外部DRAMからSRAMへ移動する必要がある。ブリッジDMAはこの目的のために実装されたものである。ブリッジDMAは2つの独立したパスを持っており、データを外部DRAMから内部SRAMへ、データを内部SRAMから外部DRAMへ移動させることができる。両方のパスを同時に動かすことはできない。BDMAは外部メモリ間でのデータ移動や内部メモリ間でのデータ移動にも使用できる。

ブリッジDMAは2つのDMAインタフェースを持っており、それぞれ外部DRAMと内部SRAMに接続されている。それぞれのインタフェースはReadとWriteリクエストをサポートしている。どちらのインタフェースもデータ幅は512ビットであり、最大バースト長は4である。

キューブのデータを移動するため、BDMAはアドレスが飛んでいる複数のラインをフェッチするラインリピート機能をサポートしている。 これによりサーフェイスを構成できる。 また、BDMAは複数のレイヤの繰り返し転送をサポートする予定である。 これにより複数のラインをフェッチしてリピートすることにより複数のサーフェイスを構成し、キューブ形状を移動できる。

img

図39. ブリッジDMA

畳み込みパイプライン

概要

畳み込みパイプラインはNVDLAコア論理の中のパイプラインの1つである。これは畳み込みアルゴリズム高速化するために使用される。様々なサイズの畳み込みをサポートするためにプログラマブルなパラメータをサポートしている。WinogradやMulti-Batchといった手法を畳み込みパイプラインに適用し、MACの動作効率を向上させることができる。

畳み込みパイプラインは5つのステージを持っている。

  • 畳み込みDMA
  • 畳み込みバッファ
  • 畳み込みシーケンスコントローラ
  • 畳み込みMAC
  • 畳み込みアキュムレータ

これらはそれぞれCDMA、CBUF、CSC、CMAC、CACCと呼ばれる。 各ステージは独自のCSBスレーブポートを持ち合わせており、制御CPUからコンフィグレーションデータを受け取ることができる。全てのステージでは同一クロックが使用される。

畳み込みパイプラインは3種類の操作をサポートしている。

  • DCモード : 特徴データのダイレクトな畳み込み操作
  • 画像入力モード : 入力画像の畳み込み
  • Winogradモード : Winograd畳み込み

畳み込みパイプラインはint16およびfp16を実行できる1024個のMACを含んでおり、部分輪を格納するために32個のアキュムレータアレイを持っている。MACの資源は2048個のint8として構成することもできる。加えて、畳み込みバッファとして512KBのSRAMを持っており、このバッファから入力の重みとアクティベーションを読み込む。本ドキュメントの後半で、このユニットの詳細を説明する。

以下は畳み込みパイプラインのダイアグラムである。

f:id:msyksphinz:20180914012307p:plain
図40. 畳み込みパイプライン

ダイレクトな畳み込み

畳み込みパイプラインは、常に2種類の入力データを処理する。1つはアクティベーションデータ、もう1つは重みデータである。NVDLAは以下の入力パラメータを持っている。

  • 特徴データキューブのサイズ: W\times H\times C
  • 1つの重みカーネルのサイズ: R\times S\times C
  • カーネルの数: K
  • ゼロパディングのサイズ: 左側の境界LP, 右側の境界RP, 上側の境界TP, 下側の境界BP
  • 畳み込みストライド: X軸方向DX, Y軸方向DY
  • 出力データキューブのサイズW'\times H'\times C'

../../../_images/ias_image5_convolution_operation.svg

図41. 畳み込み操作

下記の図は畳み込みのストライドゼロパディングを示したものである。

img 図42. 畳み込みのストライドゼロパディング

これらのパラメータは以下の関係式を持つ:

S′=(S−1)\times DX+1

R′=(R−1)\times DY+1

W′=LP+W+RP−S′SX+1

H′=TP+H+BP−R′SY+1

C′=K

出力データキューブ内の各エレメントyと、入力特徴データキューブの各エレメントxと、重みカーネルの各エレメントwtには以下の関係がある。

[tex: y{w, h, k} = \sum{r=0}^{R−1}\sum{s=0}^{S−1}\sum{c=0}^{C−1}x(wSX−LP+r),(hSY−TP+s) c} * wt{r,s, c,k}]

上記の式中に登場するw,h,c,kはすべて0からスタートする。

上記の指揮の畳み込みを実行するために、畳み込みパイプラインはダイレクトな畳み込み(direct convolution)という手法を使用する。この手法のカギとなるアイデアは、各畳み込みカーネルの乗算の操作を、各グループが64個の乗算を含むようなグループに分割することである。基本的なルールは:

  1. 全てのMACユニットを16個のサブユニットに分割する。各サブユニットはMAC Cellと呼ばれ、64個のint16/fp16ハードウェアMACか、128個のint8ハードウェアMACを持っている。
  2. MAC Cellを複数構成することをMAC Cellアレイと呼ぶ。
  3. 全ての入力データキューブをint16, fp16, int8のための1\times 1\times 64個の小さなキューブに分割する。
  4. 全ての重みデータキューブをint16, fp16, int8のための1\times 1\times 64個の小さなキューブに分割する。
  5. 1つの小さな入力データキューブと1つの小さな重みデータキューブを掛け合わせ、累積する。これらの乗算と加算は1つのMAC cell上で行われる。
  6. 上記の計算操作を4つの演算レベル、アトミック操作、ストライプ操作、ブロック操作、チャネル操作、で組み合わせる。

4つの演算については、int6の演算モードを例にして以下で説明する。

アトミック操作

アトミック操作は直接畳み込みのベースとなるステップである。1つのアトミック操作で、各MAC cellは1つの1\times 1\times 64の重みキューブを1つの重みカーネルからキャッシュする。16個のMAC cellは、int16/fp16の場合は16個のカーネルを持つか、int8の場合は32個のカーネルを持つ。1つの1\times 1\times 64の特徴データのアトミックキューブはすべてのMAC cellで共有される。MAC cellは上記のルール5に即して演算が行われる。各MAC cellの出力は部分和(partial sum)と呼ばれる。各演算は1サイクルで実行され、各サイクルで16個の部分和が生成される。部分輪は畳み込みアキュムレータモジュールに送られ、さらなる演算に使用される。

部分和の式は、以下のように示される:

[tex:PS{w, h,k,r,s, c}= \sum{i=c}^{min(c+63, C−1)}x{(wSX−LP+r),(hSY−TP+s), i} * wt{r, s, i,kPSw, h,k}]

上記の式におけるPSは部分和を指している。変数cは常に64で割り切れる数字である。

アトミック操作のダイアグラムを以下に示す。

../../../_images/ias_image7_atomic_operation.svg

図43. アトミック操作

ストライプ操作

ストライプ操作は複数の畳み込みから生成されたアトミック操作のグループを組み合わせる。1つのストライプ操作の間には、MAC cellアレイ内の重みデータは変化しない。入力データはキューブ中でスライドしながら進んでいく。

1つのストライプ操作内では、出力キューブの位置が異なるため、それぞれの部分和を加算することができない。

ストライプ操作の長さには限界がある。最小値は16であり、これは内部で重みをフェッチするバンド幅に依存するためである。最大値は32であり、これはアキュムレータのバッファサイズに依存する。いくつかの極端な例では、操作の長さは下限値を下回る可能性がある。

以下の図では、16個のアトミック操作を含むストライプ操作の例である。パディングの大きさは0である。これは入力データキューブの革新的なスキャニングではないということを注意すること。しかし一般的には、ストライプではwの次元を先に読み込む。下記の図ではパディングが存在しないため最後の2行は最初のストライプ範囲には入らない(3x3カーネルでパディングが存在せず、w=6である場合は出力のwは4となる)。

../../../_images/ias_image8_stripe_operation.svg

図44. ストライプ操作

ブロック操作

ブロック操作は高レベルの処理であり、複数のストライプ操作から構成されている。ブロック操作中には、カーネルグループ中の各カーネルは$R\times S\times 64$個の重みエレメントを使用する。この重みは入力特徴データの小さなキューブであり、大きさは演算の結果をストライプ操作間で加算できることを保証できるサイズである。これらの値は16-32エレメントのアキュムレータである。

../../../_images/ias_image9_block_operation.svg

図45. ブロック操作

1つのブロック操作中のすべてのストライプ操作は、同じ数のアトミック操作を行っている。同一ブロック操作からの部分和は畳み込みアキュムレータでストライプ操作毎に加算される。この結果は累積和と呼ばれる。

累積和は以下の等式で表現される:

[tex: AS{w,h,k,c}= \sum{r=0}^{R−1}\sum{s=0}^{S−1}\sum{i=c}^{min(c+63, C−1)}x{(wSX−LP+r),(hSY−TP+s), i} * wt{r,s,i,k}]

上記の等式において、ASは累積和である。変数cは常に64で割り切れる数である。

チャネル操作

チャネル操作も高次元の操作である。この操作には(C+63)/64個のブロック操作が含まれている。1チャネル当たりのブロック操作は似ているが、チャネルの操作方向だけが異なっている。以下の図を参照のこと。

図46. チャネル操作

1チャネル中のすべての部分和はストライプ操作により加算される。チャネル操作の後は、畳み込みアキュムレータの結果は畳み込み操作の結果となる。

チャネル操作は、以下の等式で表現される。

[tex: y{w, h,k}= \sum{i=0}^{⌊C/64⌋−1}\sum{r=0}^{R−1}\sum{s=0}^{S−1}\sum{j=c}^{min(c+63, C−1)} x{(wSX−LP+r),(hSY−TP+s), (i64+j)}wt_{r, s, (i*64+j),k}]

上記の等式はストライプが16-32としたときの最初の畳み込みの等式と同一である。1チャネルの操作が完了すると、アキュムレータの値は書きだされて後処理に渡され、次のチャネル操作の準備に入る。

グループ操作

上記の処理は主に入力特徴データと重みデータについてであったが、出力データに対する処理が残っている。出力データに対する処理は非常にシンプルである。C’(K’)->W->H->C(K)の順番で処理を行っていく。ここでC'とK'はカーネルのグループサイズであり、int16/fp16の場合は16、int8の場合は32である。

ダイレクト畳み込みの出力の順番は特徴メモリのマッピングの順番である。

../../../_images/ias_image11_output_sequence.svg

図47. 分割のシーケンス

int8とfp16の操作

上記の説明はint16の精度をもとに説明を行っている。fp16の処理も同様であるが、int8はビット処理が異なる。

畳み込みパイプラインでは、int16/fp16用の積和演算器をint8用に2つのMACに分割する。従ってint8でのスループットはint16のスループットに対して倍増する。

下記の表は、1アトミック操作あたりのパラメータである。

畳み込み精度 有力データの要素数 カーネル当たりの重み数 カーネル 出力要素数
int16 64 1024 16 16
fp16 64 1024 16 16
int8 64 2048 32 32

Winograd 畳み込み

Winograd畳み込みはダイレクト畳み込みの性能を最適化するアルゴリズムである。畳み込みパイプラインは3\times 3\times Cのサイズのカーネルでのみサポートしている。

Winograd畳み込みの目的は、演算に必要な乗算の数を減らし、結果として与えられたMACハードウェアの数のなかで性能を大幅に向上させる効果がある。

Winograd畳み込みでは、入力と出力アクティベーションデータに対して変形を行うための加算器が余分に必要になる。

Winograd畳み込みで使用される畳み込みパイプラインの等式は以下の通りである:

[tex:\begin{equation}S= AT\left[(GgGT)⊙(CTdC)\right]A\end{equation}]

ここで、⊙は要素毎の乗算である。シンボルg3\times 3カーネルであり、d4\times 4の入力データキューブのタイルである。シンボルSgdの畳み込み演算の結果である。これは2\times 2の行列である。

f:id:msyksphinz:20180915011339p:plain

$A, G, C$は重み及び入力特徴データを変形したものである。

f:id:msyksphinz:20180915011403p:plain

[tex:U=GgGT]および[tex:V=CtdC]と仮定すると、上記の等式は

S= AT\left[U⊙V\rightA]

上記の等式によると、$A,G,C$乗算は加算器を使って構成できる。3\times 3カーネルで4つの結果を計算するのに、16個の乗算器のみ必要である。一方でダイレクト畳み込みを使用すると、36個の乗算気が必要である。従ってWinogradのアルゴリズムはダイレクト畳み込みを実行すると2.25倍の乗算器を削減できる。

$U=GgGT$のステップでは、$3\times 3$のカーネルを$4\times 4$のカーネルに変換し、入力アクティベーションキューブの$4\times 4$パッチxxx。ソフトウェアは、NVDLAを実行する前に重みの情報を変換しておく必要がある。畳み込みパイプラインは入力特徴データおよび乗算の結果の変換を行う。

ダイレクト畳み込みと異なり、Winograd畳み込みパイプラインはカーネルと入力特徴データを$4\times 4\times 4$の要素の小さなデータキューブに分割する。MAC cellに渡す前に、別の加算器を使用してこれらのキューブを$CT$と$C$に変換する。このステップはPRAと呼ばれる。

Winogradアトミック操作では、MAC cellでの64回の乗算は、ダイレクト畳み込みのように単純に加算することはできない。加算は3つのフェーズで構成される。

  • フェーズ1. チャネル内の4つの乗算の結果をそれぞれ加算する。このフェーズの出力は16個の部分和であり、$4\times 4$の行列として表現される。
  • フェーズ2. $4\times 4$の部分和の行列は、$AT$と行列積を実行する。フェーズ2の出力は8つの部分和であり、$4\times 2$の行列である。
  • フェーズ3. $4\times 2$の部分和の行列は、行列$A$と行列積を実行する。出力は4つの部分和である。

したがって、4つの部分和がアキュムレータに格納され、さらなる計算に使用される。フェーズ2とフェーズ3はPOAと呼ばれる。

Winogradモードは、さらに5つの操作を持っている。必要なパラメータを以下のテーブルに示す。

mode direct convolution direct convolution Winograd Winograd
formats int16/fp16 int8 int16/fp16 int8
small data cube per MAC cell 1x1x64 1x1x64 4x4x4 4x4x4
kernels per atomic operation 16 32 16 32
atomics operation per stripe operation 16~32 16~32 16~32 16~32
strips operation per block operation R*S R*S 1 1
blocks operation per channel operation C/64 C/64 C/4 C/4

Winograd畳み込みの出力シーケンスはダイレクト畳み込みと似ている。Winogradとの違いは以下の通りである:

  • Winograd操作では、出力の幅と高さは4で割り切れる。これは必須の条件である。これは特殊なスキャンの順番によるものである。
  • Winograd畳み込みのストライプ操作のスキャンの順番は、ダイレクト畳み込みと異なる。下記の図を参照のこと。
  • ブロック操作は1つのストライプ操作のみで構成される。
  • Winogradレイヤは常に並列に4つのラインを出力する。SDPは出力データキューブのメモリマッピングの集合であることが保証される。

../../../_images/ias_image12_scan_order_wino.svg

図.48 Winogradでのストライプのスキャン順番(W-H projection)

Deconvolution

Deconvolutionは畳み込みの特殊な形である。通常の畳み込みの逆操作のようなものである。通常の畳み込みとは異なり、deconvolutionレイヤは計算後にデータキューブを拡大する。

NVDLAアーキテクチャでは、deconvolutionはソフトウェアの機能である。HWの観点からは、SW deconvolutionレイヤは、シリアルな畳み込みレイヤとRUBIKユニットによりサポートされたcontractレイヤから構成される。

図.49 は1次元のdeconvolutionレイヤの例である。入力データキューブは$W\times 1\times 1$であり、カーネルサイズは$3\times 1\times 1$である。計算フローは畳み込みとは異なるが、最終的な結果は

$DAOUT_i = \sum{j-0}^{2}DAIN{i+j-2} * W_{2-j}$

この式は畳み込みとよく似ているが、重みの$R/S$の順番が逆である。より一般的には、$W\times H\times C$の入力データキューブと$K S\times R\times C$の式は:

$DAOUT{(w,\ h,\ k)} = \sum{x = 0}^{S - 1}{\sum{y = 0}^{R - 1}{\sum{z = 0}^{C - 1}{DAIN{(w + x + 1 - S,h + y + 1 - R,\ z)}*W{(S - 1 - x,R - 1 - y,z,k)}}}}$

式によると、3Dのdeconvolutionは$(S-1)$と$(R-1)$のゼロパディングと、逆転した$R/S$重みの順番で

../../../_images/ias_image13_1d_deconvolution.svg

図.49 1次元のdeconvolution、xストライド=1

deconvolutionのXのストライドか$Y$のストライドが1ではない場合、計算フローは少し異なる。重みカーネルは小さなカーネル集合に分割される。それぞれのカーネル集合は$X$と$Y$のストライドが1と等しいように畳み込みレイヤで処理が行われる。いくつかの畳み込みレイヤはdeconvolutionのレイヤ結果を生成するために使用される。

シリアル畳み込みレイヤの後にすべてのdeconvolutionの結果の値が計算されるが、マッピングの順番は意図したものとは異なる。 もし畳み込みの結果のキューブをC方向に追加すると、最終的な出力キューブはWinogradチャネル拡張データキューブとなる。 拡張パラメータはdeconv_x_strideとdeconv_y_strideである。

したがって、NVDLAはこれらの出力の値を並び替えて、所望のdeconvolution出力キューブを取得するためにRubikモジュール内のcontract layerという特殊なレイヤを使用する。

まとめると、NVDLAは以下の手法をもってdeconvolutionをサポートする:

  • NVDLAはdeconvolutionにおいて、1以上のストライドをサポートするために2つのステップを使用する。
  • 最初のステップはシリアル畳み込みレイヤと、逆順序のカーネルを使用する。
  • 最初のステップの出力はWinogradチャネル拡張出力データキューブである。拡張パラメータはdeconvolution xストライドと、deconvolution yストライドである。
  • 2番目のステップはRUBIKユニットで実行する。
  • RubikユニットはWinogradチャネル拡張データキューブに対して逆の操作を実行する。
  • 2番目のHWレイヤの後は、出力データキューブは所望の結果に整形される。

Convolution with Image Input

入力画像の畳み込み

NVDLAはMACの使用率を向上させるために特殊モードにおいて入力データの畳み込みをサポートしている。ここで、画像データは入力サーフェイスの一部とする。しかし、NVDLAは直接畳み込みしかサポートしない。DC, Winogradおよびdeconvolutionレイヤはピクセルフォーマットを扱うことができない。マルチバッチオプションも、画像入力ではサポートされない。

DCと比較して、画像入力では以下の点が異なる:

  • チャネルのプレ拡張。重みカーネルはチャネルのプレ拡張を実行する必要がある。これはDCモードやWinogradモードのようなものではない。

  • 畳み込みバッファのデータマッピング。畳み込みバッファでの画像データマッピングはDCおよびWinogradモードでのものとは異なる。左右パディングのすべての要素と入力ピクセルラインはCBUFエントリ中にコンパクトに格納されている。以下の画像を参考のこと。チャネルサイズが4である場合、要素のマッピング順番はR(Y)->G(U)->B(V)->A(X)である。チャネルが3の場合は、順番はR(Y)->G(U)->B(V)である。

  • ストライプ操作の晩餐。ストライプ操作の長さは64に固定される。ストライプ操作はラインをまたぐことはない。したがって、すべてのストライプ操作はCBUFエントリの最初のバイトから開始される。
  • チャネルポスト拡張による高速化。チャネルのプレ拡張を行っても、通常はカーネルのチャネルサイズは32よりも小さい。したがって、チャネルのポスト拡張は画像入力の畳み込みレイヤで非常に有用である。

../../../_images/ias_image14_pixel_mapping_in_cbuf.svg

図50. 畳み込みバッファにおけるピクセルマッピング

チャネルのポスト拡張

チャネルのポスト拡張は、画像入力の畳み込みにおいてMACの使用効率を向上させるためのオプションである。

畳み込みパイプラインでは、1つのアトミック操作で64要素のチャネルの次元が必要になる(Winogradモードの場合を除く)。 もし入力データのチャネルのサイズが64よりも小さければ、MACは100%の使用効率にはならない。 したがって、MACの効率はDCモードと画像入力モードの場合はチャネルサイズに依存する。

チャネルのポスト拡張の基本的なアイデアは、実行中にチャネルサイズを拡大するために縦方向の拡張を行うということである。

例えば、画像入力レイヤは4\times 4\times 4カーネルサイズを持っている。 もしポスト拡張が有効でないと、拡張前のチャネルのサイズは16であり、MACの使用効率は25%となる。 しかし、ポスト拡張パラメータを4に設定すると、各アトミックサイクルにおいて畳み込みパイプラインは隣接する4つのラインをフェッチすることで、C=64のラインとする。 これにより、MACの効率は100%に復帰する。

チャネルのポスト拡張にはいくつかの制限が存在する。

  • チャネルポスト拡張は、画像入力の畳み込みのみ有効でである。

  • チャネルポスト拡張は2-line拡張と4-line拡張のみサポートしている。

  • チャネルポスト拡張は拡張前のチャネルサイズと、畳み込みのxストライドにより制限される。
チャネルのポスト拡張 conv_x_stride の制限 拡張前のチャネルサイズの制限
1-line No No
2-lines (conv_x_stride * ori_channel_size) <=32 <=32
4-lines (conv_x_stride * ori_channel_size) <=16 <=16

チャネルポスト拡張の数(N)はカーネルの高さ(R)よりも小さくする必要はない。ハードウェアは自動的に冗長なサイズを検出して計算に巻き込まれることを避ける。しかし、これはこの場合はMACの使用効率が向上するわけではないということに注意する必要がある。

マルチバッチモード

NVDLAのエンジンは、マルチバッチをサポートして性能を向上させバンド幅を削減する。これは特にFully-Connected(FC)レイヤで有効である。1つのFCレイヤの出力は1\times 1\times Cのデータキューブを出力する。これはつまり、FCレイヤのすべての重みデータは1度だけ使われる。FCレイヤの1回のストライド操作は、1回のアトミック操作で羅う。これによりパイプライン中では多くのバブルが発生し、MACの効率は6.25%まで減少する。効率を向上させるために、NVDLAのエンジンはマルチバッチモードを適用することができる。

マルチバッチモードはDCモードの特殊なオプションであり、複数の入力特徴データキューブを一度にしょるすることができる。畳み込みパイプラインは複数の入力データキューブをフェッチし、1セットのカーネルに適用する。これはアトミック操作にも変更を与える。異なる入力データキューブから作られた小さなキューブはインターリーブしながらロードされ、アトミック操作に渡される。ストライプ操作は、マルチバッチのためにアトミック操作が含まれている。ストライプ内で重みは再利用されるため、重みをロードするサイクルは隠蔽され、効率が向上する。

バッチサイズ毎のストライプ操作の長さは以下の通りである:

バッチサイズ 1 2 3 4 5 6
Normal length 16 8x2 8x3 4x4 4x5 4x6
Max length 32 16x2 16x3 8x4 8x5 8x6
Batch Size 7 8 9 10 11 12
Normal length 4x7 2x8 2x9 2x10 2x11 2x12
Max length 8x7 4x8 4x9 4x10 4x11 4x12
Batch Size 13 14 15 16~32
Normal length 2x13 2x14 2x15 1xN
Max length 4x13 4x14 4x15 1xN

../../../_images/ias_image15_multi_batch.svg

図.51 マルチバッチモード

Dilation

DilationはRSのサイズのカーネルを0で拡張するためのもう1つのオプションである。この機能は必要であればSWで有効である。

以下の図は、パラメータ=3のdilationである。

../../../_images/ias_image16_weight_dilation.svg

図.52 重みのdilation

NVDLAはRSの両方の次元でサポートしている。Dilationの制約は:

  • DilationはDCモードでのみサポートしている。
  • DilationはWinogradおよび画像入力モードではサポートしていない。

消費電力について

畳み込みパイプラインは、各主要なパイプライン中にてクロックゲーティングをサポートしている。もしパイプラインステージが空いている場合、パイプライン中のデータパスはクロックゲートされる。

Linuxがブートできる自作RISC-VシミュレータをGitHubにアップロードした

自作RISC-Vシミュレータがある程度落ち着いてきたので、キリがいいところでGitHubで公開することにした。

github.com

まあSpike(riscv-isa-sim)の劣化Cloneだと言ってしまえばそれまでなので、あまり意味はないのだけれど。。。

f:id:msyksphinz:20180912003015p:plain
図. 自作RISC-Vシミュレータの外部インタフェース

使い方はGitHub上のREADME.mdに書いてある。BSDライセンスなので自由に改造してやってください。 まあこういうツールの場合BSDライセンスが良いのかどうかもよく変わっていないけれども? RISC-Vの精神に基づいてみた。

Spikeシミュレータとの差分点としては、デバッグ情報がしっかり出力できるようにした。レジスタアクセス情報からメモリアクセスの情報まで、--debugオプションによりしっかり情報を出力する。 その影響で--debugオプションをかなり遅くなってしまうのだが、それは今後の改善ということにする。

あとは関数トレースといって、関数Callのツリーを生成できるようになっている。--trace-hierにより生成できる。

テストパタンのPass状況だが、402本中まだ10本は取っていないんだな。。。 これはCompressed命令がモードによって少し細かい動きがある影響。 この辺は命令デコーダの自動生成スクリプトを改良する必要がある。

という訳で、RISC-Vに興味がある限りはメンテナンスをしていくつもり。 将来RISC-Vの実装を作ることになったら、検証用のシミュレータとして使っていきたいかも。

NVDLAの内部構成調査(8. CaffeのprotobufからVirtual Platformのログを解析する)

f:id:msyksphinz:20180911011312p:plain
図は http://nvdla.org/vp.html より抜粋。

NVDLA Virtual Platformを使ってMNISTやCIFAR-10を動作させた。 おおむね動作するようになったのだが、詳細をきちんと解析しなければ単純にアプリケーションを使って遊んだだけになってしまう。 具体的にNVDLAの内部でどのようなことが起きているのか、もう少し見てみたいと思う。

で、NVDLAのログを出力できれば良いのだが、どうやらNVDLA Virtual Platformでは環境変数を使ってログの出力制御を行うことができるらしい。

以下のマニュアルを参考にした。

  • Debbuging the Virtual Simulator

Virtual Platform — NVDLA Documentation

環境変数を使ってログの出力を制御する。

$ export SC_LOG="outfile:sc.log;verbosity_level:sc_debug;csb_adaptor:enable" -- print the register access transaction from QEMU to NVDLA
$ export SC_LOG="outfile:sc.log;verbosity_level:sc_debug;dbb_adaptor:enable;sram_adaptor:enable" -- print the memory access from NVDLA to external memory

この場合、1行目の指定をした場合はCSRの値をロギングすることができ、2行目の指定を行った場合にはメモリアクセスのログを生成できるわけだ。 今回はなるべく多くのログを採取したいので、両方指定するために、aarch64_toplevelを指定する前に以下のような環境変数を設定した。

export SC_LOG="outfile:sc.log;verbosity_level:sc_debug;csb_adaptor:enable;dbb_adaptor:enable;sram_adaptor:enable"

この後aarch64_toplevelを立ち上げる。今回はnvdla/vpディレクトリ以下でQEMUを立ち上げているので、そのディレクトリにsc.logが生成される。

早速lenetを実行して、どのようなログが生成されるのか観察してみよう。

# mount -t 9p -o trans=virtio r /mnt
# cd /mnt/
# cd tests/linux/
# insmod drm.ko
# insmod opendla.ko
# ./nvdla_runtime --loadable ../lenet/basic.nvdla  --rawdump --image ../lenet/conv_0.pgm

実行が終了すると、なんと2.7GBに及ぶログが生成された。なんだこりゃ!

$ ls -lt
合計 4197944
-rw-rw-r--. 1 msyksphinz msyksphinz 2787880942  9月 10 22:21 sc.log
drwxrwxr-x. 8 msyksphinz msyksphinz         91  9月  7 19:13 tests
...
-rw-rw-r--. 1 msyksphinz msyksphinz     148249  9月  4 12:20 install_manifest.txt
drwxrwxr-x. 2 msyksphinz msyksphinz         63  9月  3 18:24 conf
drwxrwxr-x. 2 msyksphinz msyksphinz         63  9月  3 18:24 docker
drwxrwxr-x. 3 msyksphinz msyksphinz         21  9月  3 18:24 fpga
-rw-rw-r--. 1 msyksphinz msyksphinz      10503  9月  3 18:24 LICENSE
-rw-rw-r--. 1 msyksphinz msyksphinz       3004  9月  3 18:24 README.md
$

非常に長いログなので、これを解析するツールを作ろうと思った。

とりあえず簡単なツールを作って解析してみる。

システムレジスタの一覧があるので、リストをインポートしてPythonのツールを作ってみた。 非常に適当に作ったので非常に遅いけど。。。

github.com

./nvdla_vp_logger.py sc.log > sc2.dmp.log

とりあえずCSRへの設定の部分だけ抽出してみる。 メモリのLoad/Storeのログ抽出は後回し。

<Start Reading sc.log...>
CSB[9004(CACC.S_POINTER                          )] => 0x00000000
CSB[9004(CACC.S_POINTER                          )] <= 0x00000000
CSB[7004(CMAC_A.S_POINTER                        )] <= 0x00000000
CSB[8004(CMAC_B.S_POINTER                        )] <= 0x00000000
CSB[6004(CSC.S_POINTER                           )] <= 0x00000000
CSB[5004(CDMA.S_POINTER                          )] <= 0x00000000
CSB[9000(CACC.S_STATUS                           )] => 0x00000000
CSB[7000(CMAC_A.S_STATUS                         )] => 0x00000000
CSB[8000(CMAC_B.S_STATUS                         )] => 0x00000000
CSB[6000(CSC.S_STATUS                            )] => 0x00000000
CSB[5000(CDMA.S_STATUS                           )] => 0x00000000
CSB[900c(CACC.D_MISC_CFG                         )] <= 0x00002000
CSB[9010(CACC.D_DATAOUT_SIZE_0                   )] <= 0x00170017