FPGA開発日記

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

RISC-V ISS Spike を使ってMNISTのハードウェアアクセラレーションをシミュレーションしたい

RISC-V の ISS であるSpikeは、RISC-Vの通常命令セットだけでなく、RoCCのアクセラレータをシミュレーションする機能も持っている。

この場合はアクセラレータはC++で記述する必要があるが、同じ挙動を示すC/C++のコードを使ってハードウェアの挙動をあらかじめシミュレーションできるというのは非常に意味のあることだ。

RoCCアクセラレータのイメージは以下の通りだ。命令を実行すると、アクセラレータに処理をオフロードすることができる。

f:id:msyksphinz:20170825020600p:plain

MNISTのコードをハードウェアアクセラレーションしたいと思っているので、最初にISSを使ってソフトウェアのコードとハードウェアアクセラレータのC/C++モデルが一致するかシミュレーションしておこう。

Spike ISS のRoCC拡張の方法

主に以下を参考にしてほしいのだが、一転気を付けなければならないのは、生成したlibxxx.so を $RISCV/lib に格納しておかなければ、ISSがライブラリをロードできない。 シンボリックリンクでも貼っておこう。

:~/riscv64/lib$ ls -lt
合計 36200
lrwxrwxrwx 1 msyksphinz msyksphinz       72  116 01:50 libmatrix16_rocc.so -> /home/msyksphinz/work/riscv-isa-sim-msyksphinz/build/libmatrix16_rocc.so
lrwxrwxrwx 1 msyksphinz msyksphinz       72  116 00:42 libmemtotal_rocc.so -> /home/msyksphinz/work/riscv-isa-sim-msyksphinz/build/libmemtotal_rocc.so

msyksphinz.hatenablog.com

次に内部の実装だが、以下のようにとりあえず作ってみた。3種類の命令を用意している。

  • Dot Product の長さ
  • Dot Product の2番目の引数のステップ数(行列積の計算を想定し、行列を縦方向になめる操作をできるようにしておく)
  • Dot Product計算実行

github.com

  • matrix16_rocc/matrix16_rocc.cc
class matrix16_rocc_t : public rocc_t
{
 public:
  const char* name() { return "matrix16_rocc"; }

  reg_t custom0 (rocc_insn_t insn, reg_t xs1, reg_t xs2)
  {
    reg_t total = 0;
    switch (insn.funct) {
      case 0: {
        m_length = xs1;
        break;
      }
      case 1: {
        m_v_step = xs1; break;
      }
      case 2: {
        reg_t xs1_p = xs1;
        reg_t xs2_p = xs2;
        for (reg_t i = 0; i < m_length; i++) {
          int16_t a_val = p->get_mmu()->load_int16(xs1_p); xs1_p += sizeof(int16_t);
          int16_t b_val = p->get_mmu()->load_int16(xs2_p); xs2_p += (m_v_step * sizeof(int16_t));
          total = total + ((a_val * b_val) >> 16);
        }
        break;
      }
    }
    return total;
  }

MNISTプログラムの行列積計算をハードウェアにオフロードできるように変更する。

github.com

以下のようにifdefで囲み、行列積計算の一部をオフロードした。 ROCCの命令定義は、matrix16_rocc.h で以下のように定義している。

#define rocc_dot(y, mat_A, mat_B, step, len)            \
  ROCC_INSTRUCTION(XCUSTOM_DOT, y, len,   0,     0);    \
  ROCC_INSTRUCTION(XCUSTOM_DOT, y, step,  0,     1);    \
  y = 0;                                                \
  ROCC_INSTRUCTION(XCUSTOM_DOT, y, mat_A, mat_B, 2);
  • オフロードする関数の本体 (train_twolayernet_fix16.c)
fix16_t affine (const int output_size,
                           const int input_size,
                           const int batch_size,
                           fix16_t *out,            // [batch_size][output_size],
                           const fix16_t *in_data,  // [batch_size][input_size],
                           const fix16_t *wh,       // [input_size][output_size],
                           const fix16_t *wb)       // [output_size]
{
  for (int b = 0; b < batch_size; b++) {
        for (int o = 0; o < output_size; o++) {
#ifdef ROCC_MATRIX16
      rocc_dot (out[b * output_size + o],
                &in_data[b * input_size],
                &wh[o],
                output_size,
                input_size);
          out[b * output_size + o] = fix16_add (out[b * output_size + o], wb[o]);
#else // ROCC_MATRIX16
          out[b * output_size + o] = 0;
          for (int i = 0; i < input_size; i++) {
                out[b * output_size + o] = fix16_add (out[b * output_size + o],
                                              fix16_mul (in_data[b * input_size + i], wh[i * output_size + o]));
          }
          out[b * output_size + o] = fix16_add (out[b * output_size + o], wb[o]);
#endif // ROCC_MATRIX16
        }
  }
}

とりあえずここまで突貫工事で作ってみたが、推論動作はうまくいかなかった。おそらく半精度の加減乗除が間違ってるんだな。 あとで修正する。

./spike --extension=matrix16_rocc --extlib libmatrix16_rocc.so  /home/msyksphinz/work/training/risc-v/mnist/train_twolayernet_fix16_hw
...
Correct = 15
Fail = 16
Fail = 16
Fail = 16
Fail = 16
Correct = 16
Fail = 17
Fail = 17
Fail = 17
Final Result : Correct = 17 / 200
Time = 001ebf3c - 0000272e = 001e980e

関連記事

「量子コンピュータが人工知能を加速する」を読了

量子コンピュータが最近少し話題なので、向学のためにも知識を身につけておきたいのだがいきなり技術的な話は分からない。 何から手を付けていけばいいのかわからないので、とりあえずAmazonで文系的な本を見つけて買って読んだ。

ちなみに購入したのはAmazonではなく近くの本屋である。

量子コンピュータが人工知能を加速する

量子コンピュータが人工知能を加速する

そもそもプログラミングモデルが違うと言うか、組み合わせ問題を高速に説くことができるということでそれ以外の手続き型の問題を適用するのは難しいのだが、それをどのようにしてプログラムに落とし込むのか、量子ゲート方式と量子アニーリング方式でプログラミングモデルに違いがあるのか、とかをもう少し深堀して学んでみたくなった。

D-Waveの実機が無くても、シミュレーテッドアニーリングなどを気軽に試すことができる環境があれば試してみたい。 新しいプログラミングモデルはとても興味があって面白そうだ。

まあそもそも、MicrosoftのQ#に興味があるのでそれを勉強するために、量子コンピュータの基礎を学ぶために買ってみたのだけれどもね。 次はもう少し技術的なところに深堀していっても良さそうかな。

qiita.com

Quantum Development Kit | Microsoft

ちなみにもう一冊ディープラーニングの技術的な本を買ってみたのだがこちらはハズレであった。 やはり自分で検索しながら調査していかないとなあ。。

ゼロから学ぶ畳み込みニューラルネットワーク 調査中

RISC-V で MNIST を実行できるようになったので、次はCNNを実行してみたい。

多くのCNNのコードはPythonで記述してあるのだが、もう少しバイナリに近い言語で書いてあったほうが解析とRISC-Vの移植がやりやすい。

師匠のブログを読みながら、少しずつ進めてみることにする。

まずはディープラーニングC++実装ということで以下を調査してみた。

C++で学ぶディープラーニング

C++で学ぶディープラーニング

が、これはどうもCUDAが用意されていることが前提で、Virtual Box上でUbuntuを実行している環境ではCUDAを実行することができない。 一応買ってみたものの、あまり使えないなあ。

C/C++フレームワークということで色々調べているのだが、とりあえずx86で動かすならこんなのがお手軽かなあ。

github.com

ゼロから学ぶディープラーニング

第7章あたりを読み直している。

f:id:msyksphinz:20180113222247p:plain

tinyDNN を使って、MNIST を実行する

github.com

MNISTを学習するためのネットワークは、以下のように記述されている。

  • tiny-dnn/examples/mnist/train.cpp
  using fc = tiny_dnn::layers::fc;
  using conv = tiny_dnn::layers::conv;
  using ave_pool = tiny_dnn::layers::ave_pool;
  using tanh = tiny_dnn::activation::tanh;

  using tiny_dnn::core::connection_table;
  using padding = tiny_dnn::padding;

  nn << conv(32, 32, 5, 1, 6,   // C1, 1@32x32-in, 6@28x28-out
             padding::valid, true, 1, 1, backend_type)
     << tanh()
     << ave_pool(28, 28, 6, 2)   // S2, 6@28x28-in, 6@14x14-out
     << tanh()
     << conv(14, 14, 5, 6, 16,   // C3, 6@14x14-in, 16@10x10-out
             connection_table(tbl, 6, 16),
             padding::valid, true, 1, 1, backend_type)
     << tanh()
     << ave_pool(10, 10, 16, 2)  // S4, 16@10x10-in, 16@5x5-out
     << tanh()
     << conv(5, 5, 5, 16, 120,   // C5, 16@5x5-in, 120@1x1-out
             padding::valid, true, 1, 1, backend_type)
     << tanh()
     << fc(120, 10, true, backend_type)  // F6, 120-in, 10-out
     << tanh();

Convolutional Network が構成されている(よね?)ので、これもCNNということができるかな。 プログラムを実行するとLeNetというファイルが生成されたので、おそらくCNNだろう。

とりあえずMNISTのデータセットをダウンロードしてやってみる。 なぜかデータセットのファイル名が一致しなくてファイル名を修正しなければならなかった。

cd mnist/images
wget http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
wget http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz
wget http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz
wget http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz
mv t10k-images-idx3-ubyte t10k-images.idx3-ubyte
mv t10k-labels-idx1-ubyte t10k-labels.idx1-ubyte
mv train-images-idx3-ubyte train-images.idx3-ubyte
mv train-labels-idx1-ubyte train-labels.idx1-ubyte
cd -

猛烈に時間がかかるので、epoch回数を減らして、トレーニング数を減らして実行してみる。

$ ./example_mnist_train --data_path mnist/images --learning_rate 1 --epochs 3 --minibatch_size 23 --backend_type internal
Running with the following parameters:
Data path: mnist/images
Learning rate: 1
Minibatch size: 23
Number of epochs: 3
Backend type: Internal

load models...
start training

0%   10   20   30   40   50   60   70   80   90   100%
|----|----|----|----|----|----|----|----|----|----|
***************************************************Epoch 1/3 finished. 40.9915s elapsed.
9442/10000

0%   10   20   30   40   50   60   70   80   90   100%
|----|----|----|----|----|----|----|----|----|----|
***************************************************Epoch 2/3 finished. 42.4635s elapsed.
9593/10000

0%   10   20   30   40   50   60   70   80   90   100%
|----|----|----|----|----|----|----|----|----|----|
***************************************************Epoch 3/3 finished. 116.432s elapsed.
9645/10000

0%   10   20   30   40   50   60   70   80   90   100%
|----|----|----|----|----|----|----|----|----|----|
end training.
accuracy:96.45% (9645/10000)
    *     0     1     2     3     4     5     6     7     8     9
    0   969     0     6     1     1     5     8     2     7     4
    1     0  1118     0     0     0     2     4     5     0     8
    2     0     2   986     5     3     0     0    21     1     0
    3     0     4    11   975     0    15     0     4     5     9
    4     0     0     2     1   946     0     1     3     4     9
    5     1     1     0     8     0   848     6     0     4    10
    6     3     2     2     1     9     9   935     0     4     1
    7     1     0     8     7     0     2     0   972     5     4
    8     4     8    16     8     3     6     3     2   941     9
    9     2     0     1     4    20     5     1    19     3   955

example_mnist_test を実行する

早速トレーニングデータを使って実験してみよう。以下の画像データをダウンロードして実行してみる。

f:id:msyksphinz:20180113221956p:plain

$ wget https://github.com/tiny-dnn/tiny-dnn/wiki/4.bmp
$ ./example_mnist_test mnist/4.bmp
4,86.7497
7,84.1264
9,72.9942

CIFAR-10 のトレーニングと実行

同様に、 CIFAR-10 を使ってトレーニングと推論を実行してみる。

$ cd cifar10/images/
$ wget https://www.cs.toronto.edu/~kriz/cifar-10-binary.tar.gz
$ tar xvfz cifar-10-binary.tar.gz
cifar-10-batches-bin/
cifar-10-batches-bin/data_batch_1.bin
cifar-10-batches-bin/batches.meta.txt
cifar-10-batches-bin/data_batch_3.bin
cifar-10-batches-bin/data_batch_4.bin
cifar-10-batches-bin/test_batch.bin
cifar-10-batches-bin/readme.html
cifar-10-batches-bin/data_batch_5.bin
cifar-10-batches-bin/data_batch_2.bin

トレーニングを実行する。非常に時間がかかる。

$ ./example_cifar_train --data_path cifar10/cifar-10-batches-bin --learning_rate 0.01 --epochs 3 --minibatch_size 20 --backend_type internal
Running with the following parameters:
Data path: cifar10/cifar-10-batches-bin
Learning rate: 0.01
Minibatch size: 20
Number of epochs: 3
Backend type: Internal

load models...
start learning

0%   10   20   30   40   50   60   70   80   90   100%
|----|----|----|----|----|----|----|----|----|----|
***************************************************
Epoch 1/3 finished. 473.216s elapsed.
4794/10000

0%   10   20   30   40   50   60   70   80   90   100%
|----|----|----|----|----|----|----|----|----|----|
***************************************************
Epoch 2/3 finished. 454.429s elapsed.
5186/10000

0%   10   20   30   40   50   60   70   80   90   100%
|----|----|----|----|----|----|----|----|----|----|
***************************************************
Epoch 3/3 finished. 469.389s elapsed.
5433/10000

0%   10   20   30   40   50   60   70   80   90   100%
|----|----|----|----|----|----|----|----|----|----|
end training.
accuracy:54.33% (5433/10000)
    *     0     1     2     3     4     5     6     7     8     9
    0   535    15    68    23    39     7     5     9    70    24
    1    66   722    26    23    21     8    20    14   101   190
    2    46     3   301    42    96    47    34    15    13     7
    3    52    25   164   509   120   265   131   101    40    37
    4    13     4    86    28   276    32    36    33     5     3
    5     9     3   106   136    78   436    25    65     5     8
    6    24    17   129   102   209    60   701    45     9    34
    7    31    19    71    88   128   118    20   665    16    34
    8   149    43    25    12    19    11     8     9   679    54
    9    75   149    24    37    14    16    20    44    62   609

以下の画像を入力して実行してみる。画像の抽出は以下のサイトを参考にさせてもらった。

f:id:msyksphinz:20180113230026p:plain ← なんじゃこりゃ?

xiaoxia.exblog.jp

$ ./example_cifar_test cifar10/cifar-10-batches-py/data_batch_1.0_leptodactylus_pentadactylus_s_000004.png.bmp
3,81.7409
2,60.2516
8,55.0314

でも、これ合ってないなあ。。。もうちょっと学習の回数を増やそう。

Rocket-ChipでMNISTのプログラムを動かす (2. RTLシミュレーション・FPGA動作)

Rocket-Chipでディープラーニング的なプログラムを動かして、RoCCのアクセラレータで高速化することができないか、いろいろ試行している。

まずは前哨戦として、C/C++で作成したMNISTのプログラムを動かしてRISC-Vで動作させたいと思っている。 RISC-Vでコンパイルしたプログラムを動作確認させる場合の方法はいろいろあって、

  • SpikeなどのISSで流して挙動を確認する。
  • Rocket-ChipのRTLシミュレーションで挙動を確認する。
  • FPGAで動かして挙動を確認する。
  • HiFive1などのASICを動かして挙動を確認する

などの方法がある。この中で、HiFive1以外は同じ動きを確認できるはずだ(HiFive1は32-bitなので同じプログラムが動かない)。

関連記事

Rocket-ChipでRTLシミュレーションを実行するときになる、ローディングが遅すぎる問題

Rocket-ChipのRTLとシミュレーション結果を解析するとわかるが、Rocket-Chipはメインのプログラムに入る前に、DPIを使ってbinファイルからテキストとデータを抽出し、RTL側に流し込み、次にリセットを解除してプログラムを動かす、という結構面倒なことをやっている。 このときに問題になるのが、流すべきバイナリのサイズが異常に大きくなってしまうと、なかなかプログラムが始まらないということだ。 ディープラーニング系のプログラムではこれは引っ掛かりやすいだろう。

私も同じだ。MNISTの学習データを乗せただけでもRocket-Chipのローディング時間が非常に長くなり、普通には動作しなくなった。 私のMNISTのプログラムは200枚の画像を使って動作をテストしているが、200枚の画像を全部ロードすると全くRocket-Chipがスタートしない。 これは問題だ。

RISC-V のISS であるSpikeを使用して、とりあえずの挙動を観察する。

次に、Spikeシミュレータを使ってシミュレーションをしてみる。 今のところはこれが一番お手軽だ。非常に高速で、動作を簡単に確認することができる。

$ spike ./train_twolayernet_fix16_full
=== TestNetwork ===
=== TestNetwork ===
 === start ===
Correct = 0
Correct = 1
Correct = 2
...
Correct = 182
Correct = 183
Correct = 184
Final Result : Correct = 185 / 200
Time = 0d0ed1dd - 0000272e = 0d0eaaaf

200毎のMNISTのプログラムなんて、一瞬で解析してしまう。非常に便利。

画像のサイズを落としてどうにかしてRTLシミュレーションを実行する

どうにかしてバイナリのサイズを減らし、Rocket-ChipのRTLシミュレーションで挙動を確認できないだろうか。 ということで、入力する画像のサイズを減らして、とりあえず実行できるようにする。 今回は画像のサイズを減らして、ロードするバイナリのサイズを減らす。 train_twolayernet_fix16_fullが画像データをすべてロードしたもので、train_twolayernet_fix16が画像のサイズを減らして、最初の6枚だけテストを行う。

$ ls -lt train_twolayernet_fix16 train_twolayernet_fix16_full
-rwxrwxr-x 1 msyksphinz msyksphinz 8043496  113 15:09 train_twolayernet_fix16_full
-rwxrwxr-x 1 msyksphinz msyksphinz  207488  113 15:09 train_twolayernet_fix16

画像の少ない train_twolayernet_fix16 を使うと、非常に時間がかかるがシミュレーションをできるようになった。 この方法のメリットは、命令の実行ログが残っているので性能解析が行いやすい点だ。

$ make CONFIG=DefaultConfig output/train_twolayernet.riscv.out
./emulator-freechips.rocketchip.system-DefaultConfig +max-cycles=100000000 +verbose output/train_twolayernet.riscv 3>&1 1>&2 2>&3 | /home/msyksphinz/riscv64/bin/spike-dasm  > output/train_twolayernet.riscv.out && [ $PIPESTATUS -eq 0 ]
=== TestNetwork ===
=== TestNetwork ===
 === start ===
Correct = 0
Correct = 1
Correct = 2
Correct = 3
Correct = 4
Fail = 5
Correct = 5
Time = 0114d6ae - 003e0a7d = 00d6cc31

FPGAにRocket-Chipを実装して動作させる

最後はRTLではなく、FPGAにRocket-Chipを実装して動作させる方法だ。

Rocket-ChipのFPGAへの実装方法は、本ブログでも頻繁に取り上げているので、以下を参照してほしい。

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

FPGAならば実機なので、非常に高速だ。200毎のMNISTのテストも普通に実行することができた。 正しく動作しているようだ。次は、RTLシミュレーションで取得したログを用いて、性能解析に進んでいこう。

f:id:msyksphinz:20180113153545g:plain

Rocket-ChipでMNISTのプログラムを動かす (1. コンパイルと Spikeによるシミュレーション)

Rocket-Chipでディープラーニング系のプログラムを動かしたいと思っている。

まずは、Rocket-Chipで通常のMNISTプログラムを移植して動かしていこう。 これまでに作ったHiFive1のMNISTのプログラムを移植して、まずはアクセラレータを使わずに動作させたい。

RISC-V toolchainでmallocなどの関数を使いたい

もともとHiFive1用に作ったプログラムはmallocが使われていたので、何かカラクリがあるはずだ。 このあたりは、Freedom-E-SDKに仕組みが隠されていた。

github.com

void* __wrap_malloc(unsigned long sz)
{
  extern void* sbrk(long);
  void* res = sbrk(sz);
  if ((long)res == -1)
    return 0;
  return res;
}

void __wrap_free(void* ptr)
{
}
  • bsp/libwrap/sys/sbrk.c
void *__wrap_sbrk(ptrdiff_t incr)
{
  extern char _end[];
  extern char _heap_end[];
  static char *curbrk = _end;

  if ((curbrk + incr < _end) || (curbrk + incr > _heap_end))
    return NULL - 1;

  curbrk += incr;
  return curbrk - incr;
}

これらのコードは、真面目にmallocを使う代わりに、mallocを疑似的にまねる簡略的なコードだ。 mallocが複数定義されていても、wrapperを使って上書きするようなオプションがgccには備わっている。

sircmpwn.github.io

-Wl,--wrap=malloc
-Wl,--wrap=free
-Wl,--wrap=open
-Wl,--wrap=lseek
-Wl,--wrap=read
-Wl,--wrap=write
-Wl,--wrap=fstat
-Wl,--wrap=stat
-Wl,--wrap=close
-Wl,--wrap=link
-Wl,--wrap=unlink
-Wl,--wrap=execve
-Wl,--wrap=fork
-Wl,--wrap=getpid
-Wl,--wrap=kill
-Wl,--wrap=wait
-Wl,--wrap=isatty
-Wl,--wrap=times
-Wl,--wrap=sbrk
-Wl,--wrap=_exit

これで、これまでに作ったMNISTのプログラムをコンパイルし直した。 16bitの半精度浮動小数点はlibfixmathを使っている。

github.com

train_twolayernet_fix16: train_twolayernet_fix16.c syscalls.c crt.S wh1_init.o wh0_init.o wb0_init.o wb1_init.o sbrk.o malloc.o t10k-labels-idx1-ubyte.o t10k-images-idx3-ubyte.o
        riscv64-unknown-elf-gcc \
        -mabi=lp64 \
        -DPREALLOCATE=1 \
        -mcmodel=medany \
        -std=gnu99 \
        -O2 \
        -ffast-math \
        -fno-common \
        -fno-builtin-printf \
        -static \
        -nostartfiles \
        -nostdlib \
        -T ./test.ld \
        -o $@ \
        $^ \
        -lfixmath -L. \
        -Wl,--wrap=malloc \
        -Wl,--wrap=sbrk \
        -Wl,--wrap=free

SpikeでMNISTコードをシミュレーション

次にRTLシミュレーションを実行する前に、Spikeでシミュレーションをして動作を確認しておこう。

msyksphinz@msyksphinz-VirtualBox:~/work/training/risc-v/mnist$ spike train_twolayernet_fix16
=== TestNetwork ===
 === start ===
Correct = 185
Time = 0000000000000000 - 0000000000000000

rdcycleなどのレジスタはSpikeで動かないみたいなので、ここは省略している。200個の画像をテストして185個正解しているので、MNISTのコードは正しく動いているようだ。 よしよし。

f:id:msyksphinz:20180112003111p:plain
f:id:msyksphinz:20180112003102p:plain

関連記事

Computer Architecture 6th Editionの7章"Domain-Specific Architecture" を読む (7.7章 Pixel Visual Core, パーソナルモバイル画像処理ユニット 続き)

Computer Architecture, Sixth Edition: A Quantitative Approach (The Morgan Kaufmann Series in Computer Architecture and Design)

Computer Architecture, Sixth Edition: A Quantitative Approach (The Morgan Kaufmann Series in Computer Architecture and Design)

7.7章は DSAの一つの例としてPixel Visual Coreを取り上げている。前回の続き。

あまりネット上で見たことのない情報が公開されているが、参考文献はどこなのだろうか? あるいは、David.A Patterson 先生はGoogleにも所属しているので、そこからの情報なのかな。 Pixel Visual Coreの内部情報が、かなり詳細に乗っている。

目次

これは著者が読んだ内容をまとめているだけなので、誤訳、理解不足により誤っている可能性があります!鵜呑みにしないようにお願いします。


7.7章 Pixel Visual Core, パーソナルモバイル画像処理ユニット

(続き)

Pixel Visual Coreのプロセッサ

16×16個のPEと1次元当たり4つのHaloによって、PEアレイもしくはベクトルアレイと呼ばれ、Pixel Visual Coreの主要な計算ユニットである。 シートジェネレータ(Sheet Generator: SHG)というロードストアユニットを持っている。 SHGは1×1から256×256ピクセルのブロックのメモリ中で参照することができる。

Pixel Visual Core内部は2コア以上のPEを含んでいるので、PE間はNOCで接続されている。 NOCは基本的に隣接PEとの接続する構成となっている。 このNOCは2次元メッシュ状に構成されている。

Pixel Visual Coreはスカラレーンというプロセッサも持っている。 このスカラレーンは、ベクトルアレイに普通のプロセッサのように、ジャンプ、分岐、割り込み、命令制御フローなどの処理を実行してはならない。 このスカラレーンは小さな命令メモリを持っており、これはスカラプロセッサのSIMD命令のフローに似ている。

さらに、Pixel Visual CoreはDMAエンジンを持っており、DRAMとラインバッファの転送を高速に実行することができる。

Pixel Visual Coreの命令セットアーキテクチャ

Pixel Visual Coreの命令セットはGPUのように2ステップのコンパイルプロセスを持っている。 まずはHalideのような高位言語からvISA(仮想命令セット: Virtual Instruction Set Architecture)に変換する。 Virtual Instruction SetはRISC-Vの一部にインスピレーションを受けており、仮想命令という名の通り、 - レジスタファイルは無限 - メモリサイズは無限 - DMAなどの処理は存在せずそのままメモリアクセス などの特徴を持っている。さらに基本的なISAに加えて二次元画像処理向けの命令などもはいっている。

さらにここからvISAをpISA(物理命令セット: Physical Instruction Set Architecture)に変換する。 このvISAを挟むことによって、過去のプロセッサとの互換性の問題を解決することができる。 このあたりはGPUにおけるPTXに似ている(第4章を参照のこと)。

vISAからpISAへの変換も、2ステップで変換する。 まずは「事前バインディング」を行い、次にコードにパッチを当てて「事後バインディング」を行う。 バインディングにあたりパラメータとして、STPのサイズ、Haloのサイズ等を指定する。

pISAVLIWの形式をとっており、全部で119ビットで構成される。 - 43ビット: スカラーレーン。2次元PEアレイの演算も含まれる。 - 38ビット: 2次元アレイの計算 - 12ビット: メモリアクセス - 16ビット: 即値もしくは整数レジスタ - 10ビット: 即値

演算としては、整数演算、飽和付き整数演算、論理演算、シフト、データ転送、除算やLeading Zeroなどの特殊な演算などが含まれる。

pISAVLIW形式であり、Halideのカーネルは短いため、pISAの長さは200~600命令となる。 Pixel Visual Coreは2048個のpISAを保持する命令メモリしか保持していない(28.5kB)。

スカラレーンは、ラインバッファに赤ウエスするシートジェネレータの命令を発行する。 他のPixel Visual Coreのメモリアクセスとは異なり、レイテンシは1クロック以上となる可能性があるので、DMAのようなインタフェースを持っている。 レーンは最初にアドレスと転送サイズを特殊レジスタに設定する。

Pixel Visual Coreの例

図7.36はHalideのコンパイラから生成した、vISAコードである。 この例では画像のぼかしを実行している。 この例では16bitの演算気を使って、x方向の次元を計算し、次にy方向に計算を実行している。 vISAのコードはHalideプログラムのコードにマッチしている。

// vISA inner loop blur in x dimensionns
input.b16  t1 <- _input[x*1+(-1)][y*1+0][0]
input.b16  t2 <- _input[x*1+0][y*1+0][0]
...
div.b16    t9<-t7, st8:
output.b16 _blur_x[x*1+0][y*1+0][0] <- t9
// vISA inner loop blur in y dimension
input.b16  t1 <- _input[x*1+0][y*1+(-1)][0]
input.b16  t2 <- _input[x*1+0][y*1+0][0]
...
div.b16    t9<-t7, st8:
output.b16 _blur_y[x*1+0][y*1+0][0] <- t9

Pixel Visual Core のプロセッサエレメント

Pixel Visual Coreのアーキテクチャを決めるにあたり、最も重要なのは、Haloを処理するためのサポートプロセッサがどれくらいになるかということであった。 Pixel Visual Coreは16×16PEのプロセッサアレイと、2行の追加のHaloアレイをもっており、5×5のステンシル計算が可能になっている。

試行の結果、Haloは16×16のプロセッサアレイのうち20%程度しか消費しないこと明らかになった。

Pixel Visual CoreのPEは積和演算器(Multiply-Accumulate:MAC)を中心に考えられている。 このMACは16bitの乗算器を持っており、32ビットの計算結果を出力する。 パイプラインレジスタMAC中に挿入するとそれだけで電力を消費するため、MACは1サイクルで計算されるようになっている。

PEは2つの16bit ALUをもっており、以下の計算をマイサイクル実行することができる。 - 2つの16bitの計算結果を生成することができる: A op B, C op D - 3項の演算により、1つの16bit計算結果を生成することができる: A op (C op D) - それぞれのオペランドを結合することにより、32bitの演算結果を出力することができる: "A:C op B:D"

2次元ラインバッファとコントローラ

DRAMアクセスはエネルギーを消費するため、Pixel Visual Coreのメモリシステムは、メモリアクセスの回数を最小限にするように設計されている。 そのために「2次元ラインバッファ(two-dimentional line buffer)」というものが採用されている。

ラインバッファの役割は、論理的に異なる2つのカーネルで実行された結果の画像情報などを保持し、次のカーネルに渡すことである。 これにより画像の処理結果を一時的にメモリに退避することを防いでいる。

このため、ラインバッファは以下の4つの機能をサポートしている。

  1. 様々なサイズの2次元ステンシル計算をサポートしなければならない。この計算のサイズは設計時には不明である。
  2. Haloにより、16×16のPEの計算の場合は20×20のピクセルブロックを読み込まなければならず、16×16の計算結果を出力する。
  3. DAGはプログラミングできるため、ラインバッファは任意の2コアにより割り当てられる必要性がある。
  4. いくつかのコアが同一のラインバッファからデータを読み取ることができる必要がある。したがって、ラインバッファは複数の読み込みリクエストを受け付けることができる必要がある。

Pixel Visual Coreのラインバッファは複数の読み込みリクエストを受け付けることのできる2次元FIFOとして構成され、最終的には128KB SRAMインスタンスされている。 20×20のデータを読み取って16×16のデータを書き出すような、サイズの異なる読み書きをサポートするため、FIFOは4×4のピクセルをグループとして構成されている。 ステンシルプロセッサごとに、ランバッファプール(Line Buffer Poll: LBP)が配置されており、論理的に8個のラインバッファを持つ区とができている。

LBPは3レベルの抽象化を行っている。

  1. 最上位の抽象化では、LBPコントローラは8つのLBをサポートしている。各LBは1つのFIFO書き込みを行うことができ、8つのFIFO読み込みを行うことができる。
  2. コントローラはFIFOの最初と最後のポインタを保持している。LBP内のラインバッファのサイズはコントローラにより自由に調整できる。
  3. 物理的なメモリバンクが公営刺され、バンド幅を引き出すように設計されている。Pixel Visual Coreは8つの物理的なメモリバンクを持っており、128bitのインタフェースを持っており、16KBの容量である。

Pixel Visual Coreの実装

最初のPixel Visual Coreは複数のチップに分割されていた。 これは2016年にTSMCの28nmプロセスで製造され、6×7.2mmのサイズであり、426MHzで動作した。 このチップはSIPとして512MBのDRAMを持っており、負荷に応じて187~4500mWを消費した。 チップの30%は ARMv7 A53コアの制御と、MIPI、PCIe, LPDDRインタフェースで消費された。 これらのインタフェースはチップの約半分である23mm2である。

Chiselでオリジナルデザインを開発し、Verilogを生成する(1. Chisel-template を生成する)

Chiselを使ってオリジナルデザインを作成してみたい。とりあえず、ディープラーニングをターゲットとして、行列計算のためのモジュールをいろいろ作ってみたい。

まずは、Chisel単体で開発環境とテスト環境を構築するためにはどうしたらよいのだろうか。 いくつか環境を調査した。

Chisel-template を使った環境構築

Chiselを使ってVerilogファイルを生成するためには、

  1. Chiselを記述する
  2. テストを行う
  3. Chisel → (FIRRTL) → FIR → Verilog を生成する

の手順を踏む。このためには、 github で構築されている chisel-template リポジトリを使うのが便利だ。

github.com

Chisel-template を使う場合の注意

まず、Rocket-Chipの環境と共存するのは難しいと考えたほうがよさそうだ。

Rocket-Chipの環境もChiselを使うが、そのときのScala環境などの情報は~/.ivy2というディレクトリに格納されているようだ。 しかし、Chisel-templateとRocket-Chipでは、この~/.ivy2で使用する環境のバージョンが異なるようで、仕方がないので別のVirtualBox環境を構築してしまった。 2つもLinux環境を構築できない場合は、別のアカウントを作ってもよいかもしれない。

FixMadd デザインの開発

ここでは、8bitの入力値を2つ受け取り、その乗算結果を内部に格納された16bitのレジスタと加算する積和演算回路を作ってみよう。

z = z + x *y の回路を作成する。

というわけで作ってみた。Chisel-templateの環境に乗せる形で作成した。GCDのサンプルプログラムがあるので、それをまねする形で作っていく。

github.com

├── src
│   ├── main
│   │   └── scala
│   │       └── fixmadd
│   │           └── FixMadd.scala
│   └── test
│       └── scala
│           └── fixmadd
│               ├── FixMaddMain.scala
│               └── FixMaddUnit.scala

メインのプログラムは以下となる。

/**
  * Compute 8-bit MADD with 16-bit output
  */
class FixMadd extends Module {
  val io = IO(new Bundle {
    val in0       = Input(UInt(8.W))
    val in1       = Input(UInt(8.W))
    val clear     = Input(Bool())
    val in_valid  = Input(Bool())
    val out0      = Output(UInt(16.W))
    val out_valid = Output(Bool())
  })

  val r_acc        = Reg(UInt(16.W))
  val r_mul_result = Reg(UInt(16.W))
  val r_in_valid   = Reg(Bool())
  val r_out_valid  = Reg(Bool())

  r_in_valid  := io.in_valid
  r_out_valid := r_in_valid

  when (io.clear) {
    r_acc        := 0.U
    r_mul_result := 0.U
    r_in_valid   := 0.U
    r_out_valid  := 0.U
  }

  when (io.in_valid) {
    r_mul_result := io.in0 * io.in1
    printf("r_mul_result = %d\n", r_mul_result)
  }

  when (r_in_valid) {
    r_acc := r_acc + r_mul_result
  }

  io.out0 := r_acc
  io.out_valid := r_out_valid
}

シミュレーションとテスト

シミュレーションは以下のようなプログラムを書いて、テストパタンを作成した。

class FixMaddUnitTester(c: FixMadd) extends PeekPokeTester(c) {
  // Madd計算の検証用のテストパタン
  def computeFixMadd(in0: Int, in1: Int, out: Int): (Int) = {
    var resultInt: Int = out + in0 * in1
    val max_val: Int = Math.pow(2, 16).asInstanceOf[Int]
    if (resultInt >= max_val) {
      resultInt = resultInt - max_val
    }
    resultInt
  }

  private val fixmadd = c
  var expected_fixmadd: Int = 0

  // 内部信号のリセット
  poke(fixmadd.io.clear, 1)
  step(1)
  poke(fixmadd.io.clear, 0)

  // i, jの2値を振ってテストパタンを入力する
  for(i <- 1 to 40 by 3) {
    for (j <- 1 to 100 by 7) {
      // in0, in1 を設定する
      poke(fixmadd.io.in0, i)
      poke(fixmadd.io.in1, j)
      // in_valid を設定して 0→1→0 として入力を有効化する
      poke(fixmadd.io.in_valid, 1)
      step(1)
      poke(fixmadd.io.in_valid, 0)
      // Chisel で記述された FixMadd を実行する
      expected_fixmadd = computeFixMadd(i, j, expected_fixmadd)

      step(1)
      // テスト結果を比較する
      expect(fixmadd.io.out0,      expected_fixmadd)
      expect(fixmadd.io.out_valid, 1)
    }
  }
}

テストパタンを実行する

以下のように入力する。

sbt 'testOnly fixmadd.FixMaddTester -- -z Basic'

テストに成功すると、下記のメッセージが出力されて、テストが成功する。

...
[info] [0.444] RAN 421 CYCLES PASSED
[info] FixMaddTester:
[info] FixMadd
[info] FixMadd
[info] Basic test using Driver.execute
[info] - should be used as an alternative way to run specification
[info] using --backend-name verilator
[info] running with --is-verbose
[info] running with --fint-write-vcd
[info] using --help
[info] ScalaTest
[info] Run completed in 3 seconds, 77 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 29 s, completed Jan 8, 2018 11:37:15 PM

Verilogを生成する

以下のように入力すると Verilog コードが生成される。

sbt test

確認してみよう。

$ find . -name *.v
./test_run_dir/fixmadd.FixMaddTester847075120/FixMadd.v
./test_run_dir/fixmadd.FixMaddTester2058266144/FixMadd.v
  • test_run_dir/fixmadd.FixMaddTester847075120/FixMadd.v
module FixMadd(
  input         clock,
  input         reset,
  input  [7:0]  io_in0,
  input  [7:0]  io_in1,
  input         io_clear,
  input         io_in_valid,
  output [15:0] io_out0,
  output        io_out_valid
);
  reg [15:0] r_acc; // @[FixMadd.scala 20:25]
  reg [31:0] _RAND_0;
  reg [15:0] r_mul_result; // @[FixMadd.scala 21:25]
  reg [31:0] _RAND_1;
  reg  r_in_valid; // @[FixMadd.scala 22:25]
  reg [31:0] _RAND_2;
...

Vivadoで論理合成試行

Vivadoで論理合成を試行してみた。特に問題なく合成できるようだ。

github.com

  • タイミング
------------------------------------------------------------------------------------------------
| Design Timing Summary
| ---------------------
------------------------------------------------------------------------------------------------

    WNS(ns)      TNS(ns)  TNS Failing Endpoints  TNS Total Endpoints      WHS(ns)      THS(ns)  THS Failing Endpoints  THS Total Endpoints     WPWS(ns)
    -------      -------  ---------------------  -------------------      -------      -------  ---------------------  -------------------     --------
      2.284        0.000                      0                   49        0.152        0.000                      0                   49        2.000
  • 面積
+-------------------------+------+-------+-----------+-------+
|        Site Type        | Used | Fixed | Available | Util% |
+-------------------------+------+-------+-----------+-------+
| Slice LUTs*             |   89 |     0 |     53200 |  0.17 |
|   LUT as Logic          |   89 |     0 |     53200 |  0.17 |
|   LUT as Memory         |    0 |     0 |     17400 |  0.00 |
| Slice Registers         |   34 |     0 |    106400 |  0.03 |
|   Register as Flip Flop |   34 |     0 |    106400 |  0.03 |
|   Register as Latch     |    0 |     0 |    106400 |  0.00 |
| F7 Muxes                |    0 |     0 |     26600 |  0.00 |
| F8 Muxes                |    0 |     0 |     13300 |  0.00 |
+-------------------------+------+-------+-----------+-------+

関連記事

"FPGA開発日記"でのChiselを取り扱った記事。