FPGA開発日記

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

RISC-V Vector命令をサポートした自作命令セットシミュレータの実装検討 (4. Unit Strideメモリアクセスの実装)

自作命令セットシミュレータのRISC-V Vector Extensionサポート、とりあえずCSR命令の確認が終わったので先に進めていく。

RISC-V Vector Extensionのメモリアクセスにはいくつか種類があって、大きく分けると3種類。

  • Unit Stride:最もシンプルなメモリアクセス。決まったサイズのまとまったブロックのメモリを扱う。
  • Strided:各レジスタに対して単一の距離を置いたメモリを扱う。
  • Indexed:各レジスタに対してばらばらの距離を置いたメモリを扱う。

に分けられる。

分かりにくいので図を使って説明するが、Unit Strideというのは一番単純なやつ。特定のメモリブロックに対して連続したメモリ領域を取り扱う。

f:id:msyksphinz:20200711103735p:plain:w500
Unit Stride Memory Access

Stridedというのはスカラレジスタにより指定された単一距離で分離されるメモリに対してアクセスを行う。たぶん世の中的にScatter/Gatherと呼ばれるやつ。

f:id:msyksphinz:20200711104514p:plain:w500
Strided Memory Access

IndexedというのはScatter/Gatherをさらに複雑にしたもので、ベクトルレジスタにより各エレメントに対するオフセットを独立に指定できる。もっとも複雑なパタン。

f:id:msyksphinz:20200711104139p:plain:w500
Indexed Memory Access

さらにメモリストアに関してはIndexed-UnorderdとIndex-Orderedに分かれている。Unorderedはメモリストアする要素の順番をばらばらに実行できる。これは実装最適化することを目的としているが、メモリアクセス時に正確な例外を検出することができないという問題がある。一方でIndex-Orderedはメモリストアする要素の順番は明確に決まっており、Unorderdに比べて性能は低下するが例外を正確に検出することができるというメリットがある。


で、これを自作命令セットシミュレータにどのように実装するかという話で、まずはベクトルレジスタを定義しなくてはならない。とりあえず8ビットよりも小さなSEWになる事は無いだろうということで、最小要素を8ビットとして8xVLENBx32のサイズのベクトルレジスタを用意する。

class RiscvPeThread : public EnvBase
{
 private:
  RiscvDec *m_ptr_riscv_dec;
  std::unique_ptr<DWord_t []> m_regs;   // general register
  std::unique_ptr<DWord_t []> m_fregs;  // floating point registers
  std::unique_ptr<uint8_t []> m_vregs;  // Vector Registers
  std::unique_ptr<CsrEnv>  m_csr_env;   // CSR system register information
/*!
 * create new RISCV simulation environment
 * \return RiscvPeThread structure (not formatted)
 */
RiscvPeThread::RiscvPeThread (FILE           *dbgfp,
                              RiscvBitMode_t bit_mode,
                              uint64_t       misa,
                              PrivMode       maxpriv,
                              bool           en_stop_host,
                              bool           is_debug_trace,
                              FILE           *uart_fp,
                              bool           trace_hier,
                              std::string    trace_out,
                              bool           is_misa_writable)
    : EnvBase (is_debug_trace, dbgfp, trace_hier, trace_out),
      m_bit_mode (bit_mode),
      m_en_stop_host (en_stop_host),
      m_is_misa_writable (is_misa_writable)
{
...
  m_vregs = std::unique_ptr<uint8_t []>(new uint8_t[get_VLENB() * 32]);

このレジスタにアクセスるためには、以下の3つの要素を指定する必要がある。

  • アクセスするデータ型
  • レジスタ番号
  • レジスタの要素番号

ということで、データ型についてはtempalteでパラメータ化し、それ以外の部分は引数として取り扱うことにした。

template <class T>
T RiscvPeThread::ReadVReg (RegAddr_t reg_idx, uint32_t elem_idx)
{
  T value = *((reinterpret_cast<T*>(m_vregs.get())) + reg_idx * get_VLENB() + elem_idx);
  return value;
}

template <class T>
void RiscvPeThread::WriteVReg (RegAddr_t reg_idx, uint32_t elem_idx, T data)
{
  *((reinterpret_cast<T*>(m_vregs.get())) + reg_idx * get_VLENB() + elem_idx) = data;
}

reinterpret_castとか.get()とか多用しているけど大丈夫かな... 自分で作っておいて少し心配だ...

メモリアクセスは既存の関数を使用して、まずは一般的なUnit Strideのメモリアクセスルーチンを作る。これはデータ型の大きさに関わらない一般的な共通ルーチンとする。LMULを省略しているがvlの中に入っているので大丈夫のはずだ。

template <class T>
void RiscvPeThread::MemLoadUnitStride(Addr_t mem_base_addr,
                                      const RegAddr_t rs1_addr, const RegAddr_t vd_addr,
                                      bool vm, T type) {
  const int DWIDTH = sizeof(T) * 8;
  Word_t vl;
  CSRRead (static_cast<Addr_t>(SYSREG_ADDR_VL), &vl);
  Word_t vstart; CSRRead (static_cast<Addr_t>(SYSREG_ADDR_VSTART), &vstart);
  for (int i = vstart; i < vl; i++) {
    // int elem_position_byte = i * sizeof(T) * 8;
    if (vm == 0) {
      const int midx = i / DWIDTH;
      const int mpos = i % DWIDTH;
      bool skip = ((ReadVReg<T>(0, midx) >> mpos) & 0x1) == 0;
      if (skip) {
        continue;
      }
    }

    Addr_t mem_addr = mem_base_addr + i * DWIDTH / 8;
    T  res;
    MemResult except = LoadFromBus (mem_addr, &res);
    CHECK_MEM_EXCEPTION(except, mem_addr);

    WriteVReg<T> (vd_addr, i, res);
  }
}

CHECK_MEM_EXCEPTIONについてはあまりにも煩雑なのでマクロでまとめた。

#define CHECK_MEM_EXCEPTION(cause, addr) \
    if (cause == MemResult::MemMisAlign) { \
      CSRWrite (static_cast<Addr_t>(SYSREG_ADDR_VSTART), i); \
      GenerateException (ExceptCode::Except_LoadAddrMisalign, addr); \
      return; \
    } \
    if (cause == MemResult::MemTlbError) { \
      CSRWrite (static_cast<Addr_t>(SYSREG_ADDR_VSTART), i); \
      GenerateException (ExceptCode::Except_LoadPageFault, addr); \
      return; \
    } \
    if (cause == MemResult::MemNotDefined) { \
      CSRWrite (static_cast<Addr_t>(SYSREG_ADDR_VSTART), i); \
      GenerateException (ExceptCode::Except_LoadAccessFault, addr); \
      return; \
    } \

vle8.vから順にこんな感じで実装していく。ちなみにMemLoadUnitStride()の最後に変な引数0が付いているのは、これを付けないと8ビット、16ビット、32ビットなどですべて引数の形が同じになってしまい、template化できなくなったため。仕方がないのでDummyの型が異なる引数を1つ挿入した。これ、何とかならんかな。ってかだからSpikeの実装はすべてマクロで書いてあるのかな。

void InstEnv::RISCV_INST_VLE8_V(InstWord_t inst_hex)
{
  if (!m_pe_thread->IsVECAvailable ()) {
    m_pe_thread->GenerateException (ExceptCode::Except_IllegalInst, 0);
    return;
  }

  const RegAddr_t rs1_addr = ExtractR1Field (inst_hex);
  const RegAddr_t vd_addr  = ExtractRDField (inst_hex);
  bool vm = ExtractBitField(inst_hex, 25, 25);
  Addr_t mem_base_addr  = m_pe_thread->ReadGReg<DWord_t> (rs1_addr);

  m_pe_thread->MemLoadUnitStride<Byte_t>(mem_base_addr, rs1_addr, vd_addr, vm, static_cast<Byte_t>(0));
}

テストを動かそう。以下のようなプログラムを作ってみた。単純なmemcpyだ。16ビット版と32ビット版、64ビット版も要してテストしてみる。

    .text
    .global     copy_data_vec
# void *memcpy(void* dest, const void* src, size_t n)
# a0=dest, a1=src, a2=n
#
copy_data_vec:
    mv      a3, a0          # Copy destination
_loop:
    vsetvli t0, a2, e8,m1   # Vectors of 8b
    vle8.v  v0, (a1)        # Load bytes
    add     a1, a1, t0      # Bump pointer
    sub     a2, a2, t0      # Decrement count
    vse8.v  v0, (a3)     # Store bytes
    add     a3, a3, t0      # Bump pointer
    bnez    a2, _loop      # Any more?
    ret                  # Return

チェック関数はC言語で書いた。sourceとdestinationが同一であればコピーができているとする。

int32_t dest_data[DATA_NUM];

int check_data (int32_t *vec_data, const int32_t *scalar_data, const int data_num)
{
  for(int i = 0; i < data_num; i++) {
    if(vec_data[i] != scalar_data[i]) {
      return i + 1;
    }
  }
  return 0;
}


int main()
{
  copy_data_vec(dest_data, source_data, DATA_NUM * sizeof(int32_t) / sizeof(int8_t));
  return check_data(dest_data, source_data, DATA_NUM);
}

テストを動かす。ログが大変なことになった。Spikeを確認したが同様にログが汚い。もう少し何とかならんものか。

f:id:msyksphinz:20200711110432p:plain

RISC-V Vector Extension v0.9のCSR仕様2

RISC-Vのベクトル拡張の理解に当たり、複雑怪奇なシステムレジスタを理解するのは大変だ。ここではRISC-Vベクトル拡張が備える謎のシステムレジスタについて一気に解説していきたい。

ちなみに最新のRISC-V Vector Extension 0.9をベースに解説している。


RISC-V Vector Extension 0.9では以下に示すCSRを新たに定義している。

f:id:msyksphinz:20200710010429p:plain
  • vstartシステムレジスタ

ベクトルレジスタに処理を適用する場合、最初のいくつかのベクトル要素の処理を省略するためのシステムレジスタ。vstart !=0 が設定されていると、ベクトルレジスタの最初の要素は処理が行われない。

これと似たような機能に、「ベクトルレジスタマスク」がある。ベクトルレジスタマスクはvmとして表現され、RVV 0.9では常にv0がマスクレジスタとして使用される仕組みになっている。ベクトル命令のエンコーディングのうち25ビット目がvmレジスタの有効無効に使用され、vmが適用されるとv0レジスタのエンコーディングに従って特定のベクトル要素の処理を無効化することができる。

f:id:msyksphinz:20200710232923p:plain
vle32.v vd,(rs1),v0.t    # ベクトルマスク有効
vle32.v vd,(rs1)         # ベクトルマスク無効

一方でvstartシステムレジスタは使用目的が異なる。ベクトル拡張命令ではメモリアクセスによる例外が発生した場合に例外にジャンプするが、その時に「どの場所までベクトル要素を操作したか」を記憶するためにvstartが使用される。例外処理ルーチンの中でしかるべき処理を行った後に例外から復帰して再度ベクトル命令を実行する場合に、vstart != 0ならばその要素から処理が再開される。つまり、vstartは例外から復帰をサポートするためのレジスタとなっている。

この目的に沿うため、vstartシステムレジスタは、ベクトル命令を正常に完了すると常にvstart=0に設定されるようになっている。例外が発生したときのみ設定されるという仕組みだ。

f:id:msyksphinz:20200710234521p:plain
  • vtype.vta, vtype.vmaシステムレジスタフィールド

vtypeシステムレジスタには2つのレジスタフィールドが定義されている。vtype.vmavtype.vtaだ。これは「マスクされたレジスタフィールドをどのように取り扱うか」と言うことを規定しているレジスタフィールドだ。

マスクしているレジスタフィールドに対する計算結果をどのように扱うのかについては、2つの方針が考えられるだろう。例えば

vadd.vv v3,v4,v5,v0.t

という命令を実行した場合、書き込みレジスタv3においてマスクされたフィールドは、

  • マスクされているので固定値で更新する。
  • マスクされているので前の値を保持する。

という方針が考えられる。どちらにもメリットデメリットがあるのだが、問題となるのはアウトオブオーダ実行の時で、例えばベクトルレジスタ長が非常に長く、1ベクトルレジスタを複数のレジスタに分割してリネームしている場合、サブレジスタ全てがマスクにより無効化されているとき、そのレジスタに対する固定値書き込みをしてしまうと、本当はリネームしなくても良いのにリネームをしなければならなくなる。一方でマスクした値をキープすることになると、インオーダのプロセッサでは書き込みレジスタから一度データを読み取り、値を更新し(マスクする部分は値をキープし)書き込みなおすということで読み込みポートが1つ増えてしまう。まとめると以下のようなメリットデメリットがあるといえよう。

  • マスクされているので固定値で更新する。
    • リネームをしないプロセッサではレジスタ読み込みポートを節約することができる。
    • リネームをするプロセッサではマスクにより無効なレジスタでも常にリネームをする必要がある。
  • マスクされているので前の値を保持する。
    • リネームをしないプロセッサではレジスタの部分更新を許すため読み込みポートが増える。
    • リネームをするプロセッサではマスクにより無効なレジスタはリネームをせずに済む。

ということで、RISC-V Vector Extension 0.9ではこれを切り替えることができるオプションが用意されている。これがvtype.vmaである。

  • vtype.vma=1(Agnostic):マスクにより無効な要素はすべてのビットフィールドに1が書き込まれる、もしくは値を保持する。
  • vtype.vma=0(Undisturbed):マスクにより無効な要素は値を保持する。

で、実はこのオプションはハードウェアにより決め打ちすることは出来ない。仕様上は「Agnosic, Undisturbed」の両方を実装する必要がある。しかしリネームを行うプロセッサは、Agnosticの「もしくは」を除去し「値を保持する」に決め打ちすることができる。一方でリネームをしないプロセッサではこの設定自体を「無視する」と言うことが許されている。実はとってもあいまいなレジスタ仕様になっている(これはおそらくRISC-V Vector Extension 1.0で修正されると思われる)。

f:id:msyksphinz:20200711000552p:plain

という訳でvmaの役割が分かった。で、vtaも実は同じ役割を持っており、こちらはMaskedな要素に対する仕様ではなく、Tail要素に対する仕様となっている。

ベクトルレジスタに対する「Tail」というのは、vlレジスタにより設定されている現在のベクトル要素から、物理的に取りうる最大のベクトル要素数VLMAXまでの要素のことを言う。

例えば、VLEN=512, SEW=32, LMUL=1の場合は、物理的に1つのレジスタから512/32=16本の要素を取ることができる。これがVLMAXに相当する。そして実はvlレジスタはVLMAXよりも小さな値を設定することができる。これは半端な長さのベクトルを処理するための処置で、vlレジスタに設定されたベクトルの要素数と、物理限界であるVLMAXまでの間の要素のことをTailと呼んでいる。

f:id:msyksphinz:20200711001051p:plain

このTail要素に対してもvtype.vtaシステムレジスタにより取り扱いを決めることができる。これがvtype.vmavtype.vtaの役割だ。

f:id:msyksphinz:20200711001714p:plain

RISC-V Vector Extension v0.9のCSR仕様1

RISC-Vのベクトル拡張の理解に当たり、複雑怪奇なシステムレジスタを理解するのは大変だ。ここではRISC-Vベクトル拡張が備える謎のシステムレジスタについて一気に解説していきたい。

ちなみに最新のRISC-V Vector Extension 0.9をベースに解説している。


RISC-V Vector Extension 0.9では以下に示すCSRを新たに定義している。

f:id:msyksphinz:20200710010429p:plain

Vector Extensionを理解するにあたりとりあえず無視していいのが、"vxsat", "vxrm", "vcsr"である。これらのCSRはFixed Point命令のためのレジスタなので本質にあまり関係しない。

そしていくつかVector Extension固有の定数について理解しておこう。以下に示す数値は「実装に固有の数値」であり、ハードウェアにより定義されるものだ。これらは命令の実行中に変更することはできない。

  • VLEN : ベクトルレジスタ1本が持つビット長。例えばベクトルレジスタ1本が512ビットであれば、VLEN=512である。
  • ELEN : ベクトルレジスタ内の1つの要素を示す最大のビット数。例えばベクトルレジスタで最大64ビットのデータを1要素として扱うことができるとすると、ELEN=64となる。当然、ELENはXLEN or FLENよりも大きな数値である必要がある。
  • SLEN : ベクトルレジスタ内のインデックス参照方法をストリップして表現することができる。これは複雑なので必要になれば理解すれば良い。

これを踏まえ、システムレジスタでまず理解しなければならないのはvtype CSRだ。このCSRはいろんなフィールドを持っており、理解するのが難しい。

f:id:msyksphinz:20200710010622p:plain

vsewから始める。これは現在のベクトルレジスタの1要素を示すビット長だ。RISC-V Vector Extensionでは、1命令で複数のデータ長を扱う。例えば32ビットのベクトル加算も、64ビットのベクトル加算も両方ともvadd.vvで表現されるため、今自分がどのビット長で計算しているのかが分からない。このためvtype.vsewで現在実行中のデータ長を把握する。

f:id:msyksphinz:20200710011250p:plain

もう一つ、vlmulというフィールドがある。これは、1つのレジスタインデックスで複数のベクトルレジスタを取り扱うことができるようになる。vlmulの値に伴いLMULというパラメータがエンコードされ、一度に何個のレジスタを扱うかを指定できる。大量のデータを扱い、同じ操作を複数のレジスタに対して実行する必要があるときに便利だ。

f:id:msyksphinz:20200710011539p:plain

具体的には、通常はvtype.vlmul=0(LMUL=1)の時、以下の命令は、v8レジスタとv16レジスタを読み取り、加算を行いその結果をv0レジスタに格納する。

vadd.vv v0, v8, v16   # v0 <= v8 + v16

ところが、vtype.vlmul=1(LMUL=2)とすると、vNレジスタ(Nは2の倍数でなければならない、それ以外は命令例外が発生する)とvN+1レジスタに対して操作が適用される。つまり上記のvadd.vv命令は、

vadd.vv v0, v8, v16  # {v1, v2} <= {v9,v8} + {v17,v16}

となり、1命令で複数のレジスタを扱うことができる。命令フェッチ幅を抑えるための巨大なベクトルレジスタのような使い方ができるようになる。これは最大でLMUL=8まで指定することができ、最大でvN~vN+7のレジスタを同時に扱うことができるようになる。

これを踏まえ、現在ベクトル命令が実行されると何個の要素が1つのレジスタとして扱うことができるようになるのだろうか?これを示しているのがvlレジスタである。

vlレジスタの値は:「VLEN / SEW * LMUL」で表現される。SEWvtype.sewをエンコードした値、LMULvtype.vlmulをエンコードした値である。

具体例を見る。VLEN=512SEW=64LMUL=1である場合、vl = 512 / 64 * 1 = 8であり、64ビットの値が8個まとまったものが1つのベクトルレジスタに集約されている。これは簡単。

では次に、VLEN=512SEW=32LMUL=1である場合、vl = 512 / 32 * 1 = 16となり、32ビットの値が16個まとまったものが1つのベクトルレジスタに集約されていると考えられる。つまり、SEWLMULの値によってvlの値は逐次変化していく!

さらにややこしいことに、VLEN=512SEW=64LMUL=4とすると、vl=512 / 64* 4 = 32となり、1つのレジスタインデックスを参照することで32個の要素に対する処理が行われることを意味する。このように、コンフィグレーションによりレジスタの定義範囲が大きく変わるので、現在の状態が把握しにくいのがRISC-V Vector Extensionの特徴である。

f:id:msyksphinz:20200710192203p:plain

このシステムレジスタの設定方法だが、多くの場合はvsetvli命令によって設定される。vsetvliの命令仕様についてはあまり詳細は説明しないが、Spikeの実装を参考にするのが良いと思う。

 vsetvli rd, rs1, vtypei # rd = new vl, rs1 = AVL, vtypei = new vtype setting 
 vsetvl  rd, rs1, rs2    # rd = new vl, rs1 = AVL, rs2 = new vtype value
f:id:msyksphinz:20200711003245p:plain
   // set vl
   if (vlmax == 0) {
     vl = 0;
   } else if (rd == 0 && rs1 == 0) {
     vl = vl > vlmax ? vlmax : vl;
   } else if (rd != 0 && rs1 == 0) {
     vl = vlmax;
   } else if (rs1 != 0) {
     vl = reqVL > vlmax ? vlmax : reqVL;
   }

LLVMの新しい中間言語表現 MLIRを試す(6. MLIRに関する発表資料を読む)

MLIRについてもう少し具体的な例を勉強するために、資料を読み込んでいくことにした。前回の続き。

以下の資料を参考にした。Chris Lattnerの所属がSiFiveに変わっているので、比較的最近の資料だ。

docs.google.com


MLIRとLLVM IR

MLIRをLLVM IRの代替として使う方法

f:id:msyksphinz:20200707232012p:plain:w400

MLIRにはLLVM IRを組み込むことができ、既存のLLVM IRパスを移動して再実装することができる。ただし、これには非常に膨大な作業が必要となってしまう。

f:id:msyksphinz:20200707232027p:plain:w400

LLVM IRをMLIRに移行することで、より優れたデータ構造に移行することができる。MLIR実装チームはLLVMでの多くの実装経験がある。

f:id:msyksphinz:20200707232045p:plain:w400

最大の機能は、マルチスレッドコンパイルが可能になった。MLIRは最初からこれをサポートするように設計されている。MLIR PassManagerには優れた機能が搭載されており、インスタンス固有のパスオプションと、Statistics、パイプラインクラッシュの再現の生成などが含まれている。

f:id:msyksphinz:20200707232100p:plain:w400

LLVM IRのPHIノードについて取り上げる。PHIノードをスキャンした場合、ブロックの先頭などに命令を挿入する必要がある。PHIノードのエントリは順序図消されていないため、ルックアップテーブルを利用してオペランドリストをスキャンする必要があり、これに時間がかかる。

f:id:msyksphinz:20200707232113p:plain:w400
f:id:msyksphinz:20200707232130p:plain:w400

さらに、MLIRのデバッグ情報はLLVM IRのデバッグ情報よりも優れている。

LLVMのループ最適化を再実装するよりも、MLIRを使用することで効率的に実装できる。

f:id:msyksphinz:20200707232143p:plain:w400

LLVMの新しい中間言語表現 MLIRを試す(5. MLIRに関する発表資料を読む)

MLIRについてもう少し具体的な例を勉強するために、資料を読み込んでいくことにした。前回の続き。

以下の資料を参考にした。Chris Lattnerの所属がSiFiveに変わっているので、比較的最近の資料だ。

docs.google.com


MLIRをClangで使用する

f:id:msyksphinz:20200706010509p:plain:w400

現在のClangのIR生成に関する問題。ASTから複雑な処理を通じてLLVM IRを生成するが、C++からLLVM IRの差分が大きすぎる。また、Clangコミュニティの動きが素早く、長期プランによる開発ブランチが作れない。インクリメンタルなパスを見つける必要がある。

f:id:msyksphinz:20200706010525p:plain:w400

まずは診断パスから始めることを推奨する。それを交換してアップグレードすることにより、IR生成パスを直接作るよりも障壁がはるかに低くなる。

f:id:msyksphinz:20200706010537p:plain:w400

最初はCIRの表現を定義する。CIRとは中間的な表現を意味する。

いくつかのイタレーションの後、既存のフローセンシティブなデータフロー診断を移植することは理にかなっている。これらはclangコンパイラフローで使用されるものであり、この方法の利点はMLIRを使用すること新しい診断用のテストケースを簡単に作成することができるということである。

f:id:msyksphinz:20200706010550p:plain:w400

新しいフローセンシティブな診断が動作し古いものよりも良いようであれば、これをデフォルトに設定する。これによりMLIRがプロダクションのClangパスで有効になる。正常に動作することが確認できれば、Clang CFGに基づく古い実装を削除することができる。

Clang CFGをどうするかということだが関連しているコードが大量にあるため、進行を妨げないようにするためにCSA(Clang Static Analyze)の実装の詳細になるようにCFGを移動することを推奨する。

f:id:msyksphinz:20200706010604p:plain:w400

MLIRパスがデフォルトで本番環境で有効になっているので、次はIRの生成フローで使用することを考える。このパスでは、C++のすべての機能をサポートしている必要があり、これは非常に難易度が高い。ある時点で完全に実装が完了すると、デフォルトで有効にすることができる。

f:id:msyksphinz:20200706010615p:plain:w400

LLVMの新しい中間言語表現 MLIRを試す(5. MLIRに関する発表資料を読む)

MLIRについてもう少し具体的な例を勉強するために、資料を読み込んでいくことにした。前回の続き。

以下の資料を参考にした。Chris Lattnerの所属がSiFiveに変わっているので、比較的最近の資料だ。

docs.google.com


次はTensorFlowのExampleの例を見る。TensorFlowもMLIRをサポートしているらしい。

f:id:msyksphinz:20200705000420p:plain:w400

TensorFlowのコンパイラエコシステムでは、TensorFlowのグラフからXLAのHLOに変換し、それをIRに変換しているらしい。HLOは"High Level Optimizer"のことを言うらしい。

f:id:msyksphinz:20200705000402p:plain:w400

TensorFlowのGraphをTF Graph.mlirというものに変換し、これをHLO.mlirに変換する。これをXLO HLOに変換するらしい。このTF Graph.mlirとHLO.mlirがどういうフォーマットになっているのか気になる。

f:id:msyksphinz:20200705000350p:plain:w400

TensorFlowの計算グラフをMLIRで表現すると以下のようになるらしい。これはLLVM IRと似ていると思う。

f:id:msyksphinz:20200705000339p:plain:w400

Bridgeをビルドする。

  1. グラフの変換と最適化のパスを追加する。
  2. オペレーションの書き換えルールを追加する。
f:id:msyksphinz:20200705000324p:plain:w400

次はMLIRユーザによるいくつかの例を見ていく。

  • ステンシル計算の表現形式としてMLIRを使用する。ステンシルの計算では格子の隣接したセルの計算が必要になるという訳か。
f:id:msyksphinz:20200705002445p:plain:w400

これの意味がまだ理解できない。ステンシルの計算を表現しているらしい。

f:id:msyksphinz:20200705002430p:plain:w400
  • Fortran向けの最適化コンパイラFLANG。FIR(High-level Fortran IR)をMLIR形式で表現し、これをLLVM IRに変換してアセンブリコードを生成する。
f:id:msyksphinz:20200705002418p:plain:w400
  • XilinxのAIエンジンではMLIRを使っている。
f:id:msyksphinz:20200705002405p:plain:w400
f:id:msyksphinz:20200705002355p:plain:w400

LLVMの新しい中間言語表現 MLIRを試す(4. MLIRに関する発表資料を読む)

MLIRについてもう少し具体的な例を勉強するために、資料を読み込んでいくことにした。前回の続き。

以下の資料を参考にした。Chris Lattnerの所属がSiFiveに変わっているので、比較的最近の資料だ。

docs.google.com


MLIR Infrastructureについて

f:id:msyksphinz:20200704234045p:plain:w400

この"Batteries Included"というのは、自分自身がオペレーションを定義することができる、ということだと思う。

Op definitionの例ということで、TensorFlowのLeakyReluの例が出てくる。

f:id:msyksphinz:20200704234113p:plain:w400

TableGenによって定義されるDSLなのだが、まずはTF_LeakyReluOpは"TensorFlowの単項演算子である"という定義がなされている。プロパティの設定(パラメータの設定のようなもの:NoSideEffects)が行われている。Opの定義はSameValueTypeで指定されているのだろうか。入力と出力オペランドの形はlet argumentsで指定されているようだ。最後にドキュメントが指定されている。

これにより、自動的にドキュメントが生成されるという仕組みになっているようだ。

f:id:msyksphinz:20200704234127p:plain:w400

このDSLからC++のコードが出力されるわけだが、これにはCheker(Verifier)まで一緒に実装されているらしい。アサーションが自動的に挿入されているという仕組みかな。

f:id:msyksphinz:20200704234140p:plain:w400

Opのダンプ用のコードやParserをassemblyFormatで指定することができるということか?

f:id:msyksphinz:20200704234154p:plain:w400

Declarative Rewrite Rules(DDR)。これはLLVMのRewrite Ruleと大して変わらないのだろうか?

f:id:msyksphinz:20200704234206p:plain:w400

で、このRewriteのルールを大量に記述すると動作が遅くなるらしい。これをステートマシンに変換して速度を改善すると書いてあるが、詳しいことは正直良く分からない。

f:id:msyksphinz:20200704234219p:plain:w400

mlir-optはコンパイラのPassをテストするためのフレームワーク。llvm-optと同様。

f:id:msyksphinz:20200704234230p:plain:w400

デバッグ機能。ソースロケーショントラッキング。これはLLVM IRにも付いている奴かな?

f:id:msyksphinz:20200704234245p:plain:w400

そして、MLIRはLLVM IRを方言として受け付けることができるらしい。LLVM IRは「ベクトル付きのC」と言われているくらいあって、あまりレイヤが高くないのでこれを受け取ることができる。

f:id:msyksphinz:20200704234302p:plain:w400