FPGA開発日記

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

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

RISC-Vのランダムテストriscv-tortureの+signature

f:id:msyksphinz:20180909010643p:plain

RISC-V Tortureは、生成したテストパタンをシミュレータで実行し、最後にシグニチャを出力する。 シグニチャを他の実装(Rocket-Chipなど)との結果と比較して正しく実行できたかを判定している。

SpikeおよびRocket-Chipでシグニチャを生成するためには+signature=<filename>とすればよいというのは前回解析した。 次にこのシグニチャが具体的にどのような方法で生成されているのかについて調査する。

RISC-V Tortureのシグニチャは生成されたテストパタンのbegin_signatureend_signatureの間メモリの内容がダンプされている。 また、テストパタンの最後に`レジスタの値をすべて比較該メモリ領域にダンプすることでレジスタの中身を一致比較させるようになっている。 まあ、これだと計算の最後しか見ることができないな。。。

reg_dump:
        la x1, loop_count
        lw x2, 0(x1)
        addi x3, x2, -1
        sw x3, 0(x1)
        bnez x2, pseg_0
        la x1, xreg_output_data
        sd x0, 0(x1)
        sd x2, 16(x1)
        sd x3, 24(x1)
        sd x4, 32(x1)
        sd x5, 40(x1)
        sd x6, 48(x1)
        sd x7, 56(x1)
...

従って、同じような仕組みで自作RISC-Vシミュレータにもシグニチャを生成する仕組みを入れると、Spikeとのランダムテストの一致比較ができるはずだ。早速やってみよう。

  for (Addr_t addr = m_sig_addr_start; addr <= ((m_sig_addr_end-1) | 0xf); addr += 0x10) {
    for (Addr_t b_addr = addr + 0xf; b_addr >= addr; b_addr--) {
      UByte_t b_data;
      LoadMemory<UByte_t> (b_addr, &b_data);
      fprintf (fp, "%02x", b_data);
    }
    fprintf (fp, "\n");
  }

追加したものをテストしてみる。同じパタンで、Spikeの生成したtest.spike.sigと自作シミュレータで生成したシグニチャを比較する。

$ diff
$ diff test.forest.sig test.spike.sig
22c22
< d6bd427500000006ffffffffffff8008
---
> d6bd4275000000067ff8000000000000
26c26
< 7ff0000000000000ffffffffffff8008
---
> 7ff00000000000007ff8000000000000
29c29
< 0000000000000000ffffffffffffffff
---
> 00000000000000007ff8000000000000

ありゃ!差分がある。解析が必要だなあ。

RISC-Vのランダムテストriscv-tortureを試す

f:id:msyksphinz:20180909010643p:plain

RISC-Vの実装を作るとまずはテストパタンを生成してテストを流してみたくなるのだが、RISC-Vにはテスト用のパタンがいくつか用意されている。

RISC-V Tortureのダウンロードとインストール

RISC-V TortureのConfiguration

Tortureの生成パタンの構成を変えたいときは、config/*.configを変更するとよいらしい。

  • config/default.config
torture.generator.nseqs     200
torture.generator.memsize   1024
torture.generator.fprnd     0
torture.generator.amo       true
torture.generator.mul       true
torture.generator.divider   true
torture.generator.segment   true
torture.generator.loop      true
torture.generator.loop_size 64

torture.generator.mix.xmem    10
torture.generator.mix.xbranch 20
torture.generator.mix.xalu    50
torture.generator.mix.fgen    10
...

RISC-V Tortureのテスト生成

実際にテストを生成してみた。

$ make igentest
java -Xmx1G -Xss8M -XX:MaxPermSize=128M -jar sbt-launch.jar 'testrun/run'
OpenJDK 64-Bit Server VM warning: ignoring option MaxPermSize=128M; support was removed in 8.0
[info] Loading project definition from /home/masayuki/work/riscv-torture/project
[info] Set current project to torture (in build file:/home/masayuki/work/riscv-torture/)
[info] Running torture.testrun.TestRunner
Physical mode
running:List(spike, +signature=output/test.spike.sig, output/test)
running:List(spike, +signature=output/test.spike.sig, output/test)
///////////////////////////////////////////////////////
//  All signatures match for output/test
///////////////////////////////////////////////////////
[success] Total time: 1 s, completed 2018/09/06 10:18:52

テストの中身を見てみる。アセンブラで記述してあり、ランダムパタンの様相を呈している。

// random assembly code generated by RISC-V torture test generator
// nseqs = 200
// memsize = 1024

#include "riscv_test.h"

RVTEST_RV64UF
RVTEST_CODE_BEGIN

        j test_start

crash_backward:
        RVTEST_FAIL

test_start:

freg_init:
freg_s_init:
        la x1, freg_init_data
        flw f0, 0(x1)
        flw f2, 16(x1)
        flw f4, 32(x1)
        flw f10, 80(x1)
        flw f13, 104(x1)
        flw f15, 120(x1)
...

pseg_0:
        addi x18, x0, -1973
        addi x29, x0, 1316
        sraiw x4, x28, 24
        remw x6, x30, x30
        la x5, test_memory-184
        addiw x1, x30, 1106
        fmul.d f24, f9, f1
        slti x20, x10, 2037
        sll x24, x10, x27
        fmadd.d f5, f16, f9, f3
        mulh x26, x22, x22
...

outputディレクトリが生成されていた。

$ tree output/
output/
├── Makefile
├── test
├── test.S
├── test.spike.sig
└── test.stats

テスト結果の検証については、signatureのdiffを取ってチェックするらしい。

Rocket-Chip環境でtortureテストを実施する

Rocket-Chipのリポジトリにもtortureがサブリポジトリとして格納されているので、これを使ってみよう。

cd torture
make

Verilatorでの実行が失敗するので、Makefileを以下のように変更した。

index a85579f..8138c8b 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@

 SBT ?= java -Xmx1G -Xss8M -XX:MaxPermSize=128M -jar sbt-launch.jar
 RTL_CONFIG := DefaultConfig
-C_SIM := ../emulator/emulator-rocketchip-$(RTL_CONFIG)
+C_SIM := ../emulator/emulator-freechips.rocketchip.system-$(RTL_CONFIG)
 R_SIM := ../vsim/simv-rocketchip-$(RTL_CONFIG)
 TEST := output/test.S
 OPTIONS := $(empty)

実際にテストを実行してみる。Rocket-Chipにも+signature=filenameオプションが通じるらしい。

$ make ctest
java -Xmx1G -Xss8M -XX:MaxPermSize=128M -jar sbt-launch.jar 'testrun/run -c ../emulator/emulator-freechips.rocketchip.system-DefaultConfig -a output/test.S '
OpenJDK 64-Bit Server VM warning: ignoring option MaxPermSize=128M; support was removed in 8.0
[info] Loading project definition from /home/masayuki/work/rocket-chip/torture/project
[info] Set current project to torture (in build file:/home/masayuki/work/rocket-chip/torture/)
[info] Running torture.testrun.TestRunner -c ../emulator/emulator-freechips.rocketchip.system-DefaultConfig -a output/test.S
Physical mode
running:List(spike, +signature=output/test.spike.sig, output/test)
running:List(spike, +signature=output/test.spike.sig, output/test)
running:List(../emulator/emulator-freechips.rocketchip.system-DefaultConfig, +max-cycles=10000000, +signature=output/test.csim.sig, output/test)
This emulator compiled with JTAG Remote Bitbang client. To enable, use +jtag_rbb_enable=1.
Listening on port 46292
///////////////////////////////////////////////////////
//  All signatures match for output/test
///////////////////////////////////////////////////////
[success] Total time: 25 s, completed 2018/09/06 20:29:47

Nightly Testの実施

これはランダムテストを用いたリグレッションみたいなものかな?

一度に複数のテストも生成してリグレッションテストをすることができそうだ。

$ make cnight
...
[warn] there was one deprecation warning; re-run with -deprecation for details
[warn] one warning found
[info] Running torture.overnight.Overnight -c ../emulator/emulator-freechips.rocketchip.system-DefaultConfig -g none
Physical mode
running:List(spike, +signature=output/test_1536233603600.spike.sig, output/test_1536233603600)
running:List(spike, +signature=output/test_1536233603600.spike.sig, output/test_1536233603600)
running:List(../emulator/emulator-freechips.rocketchip.system-DefaultConfig, +max-cycles=10000000, +signature=output/test_1536233603600.csim.sig, output/test_1536233603600)
This emulator compiled with JTAG Remote Bitbang client. To enable, use +jtag_rbb_enable=1.
Listening on port 45600
///////////////////////////////////////////////////////
//  All signatures match for output/test_1536233603600
///////////////////////////////////////////////////////
Physical mode
running:List(spike, +signature=output/test_1536233629933.spike.sig, output/test_1536233629933)
running:List(spike, +signature=output/test_1536233629933.spike.sig, output/test_1536233629933)

NVDLAの内部構成調査(7. Caffeのモデルを使ってNVDLAでCIFAR10を動かす)

前回、NVDLA Virtual PlatformでMNISTを動作させることができた。同じように、NVDLAでCIFAR-10も動作させることができるはずだ。

PGMは1色の画像しか認識できないので、PGMでCIFAR-10の画像を渡すことはできない。そこで、JPEG形式でNVDLAにデータを渡して推論を実行することにした。

CIFAR-10の画像は、以下のサイトからPNG形式を取得し、ImageMagickを利用してJPEGに変換した。

pjreddie.com

$ for pgm in `ls -1 *.pgm`; do convert ${pgm} +compress conv_${pgm}; done

CaffeでのCIFAR-10の学習

こちらもExampleが用意されている。 fullモードで学習を行うと非常に時間がかかるので、Quickで学習を行った。

$ data/cifar10/get_cifar10.sh
$ examples/cifar10/create_cifar10.sh
$ examples/cifar10/train_quick.sh

これでCIFAR-10のCaffeのモデルが生成されるので、nvdla_compilerコンパイルしてNVDLA用のバイナリを作成する。

nvdla_compiler --prototxt ../cifar10/cifar10_full.prototxt --caffemodel ../cifar10/cifar10_quick_iter_4000.caffemodel

basic.nvdlaが生成されたので、NVDLAに渡して実行してみる。 以下はQEMU上で実行する。

# ./nvdla_runtime --loadable ../cifar10/basic.nvdla  --rawdump --image ../cifar10/0_cat.jpg
# cat output.dimg
0.00832367 0.000235677 0.00159931 0.0301514 0.00696945 0.122803 0.000342846 0.192383 0.623535 0.0136414 #

# ./nvdla_runtime --loadable ../cifar10/basic.nvdla  --rawdump --image ../cifar10/1_ship.jpg
# cat output.dimg
0.00250626 5.126e-06 9.77516e-06 0.00281715 0.000716209 0.00065136 1.01924e-05 0.00177479 0.991211 0.000142932 #

# ./nvdla_runtime --loadable ../cifar10/basic.nvdla  --rawdump --image ../cifar10/3_airplane.jpg &> /dev/null
# cat output.dimg
0.548828 0.000263214 0.00307655 0.0654297 0.00289726 0.00300217 0.000976562 0.0108719 0.364258 0.000113666 #

CIFAR-10は以下の順番でラベルが定義されている。

カテゴリ 番号
airplane 0
automobile 1
bird 2
cat 3
deer 4
dog 5
frog 6
hourse 7
ship 8
truck 9

0_cat.jpgは推論に失敗している(9番目が選択されている)。 1_ship.jpgは9番目が選択されており、認識成功だ。 3_airplane.jpgは0番目の数値が高く認識成功だ。

f:id:msyksphinz:20180908013303p:plain