FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://sites.google.com/site/fpgadevelopindex/

プロセッサHiFive1で機械学習コードを動作させる(3. 推論コードの動作確認)

f:id:msyksphinz:20170821013230p:plain

RISC-V プロセッサHiFive1を使って、機械学習チュートリアルとも言えるMNISTのプログラムを動作させたい。 まずはMNISTの推論コード自身から、HiFive1に移植している。

当たり前と言えば当たり前だが、行列演算の途中のデータとか、全部L1データキャッシュでやりくりする訳だから、あまり大きなプログラムをロードすることはできない。 これまで大きなプログラムを動かして途中でクラッシュしてボードが動かなくなっていたのは、これではないかと予想。 そこで、これまでに関数内で確保していた行列演算の途中の変数を、仮にでもいいのでグローバル変数として確保し、あらかじめ使用するすべての変数を確保しておいた。

github.com

diff --git a/software/mnist/train_twolayernet_fix16.c b/software/mnist/train_twolayernet_fix16.c
index d35e94f..0802eff 100644
--- a/software/mnist/train_twolayernet_fix16.c
+++ b/software/mnist/train_twolayernet_fix16.c
@@ -25,7 +25,7 @@
 #define ALPHA    (10)   // Coefficient of learning
 #define SEED     (65535)  // Seed of random
 #define MAXINPUTNO (60000)  // Max number of learning data
-#define BATCH_SIZE (100)
+#define BATCH_SIZE (10)
 #define LEARNING_RATE (0.1)
 #define WEIGHT_INIT (0.01)
 
@@ -286,6 +286,13 @@ int main ()
   return 0;
 }
 
+fix16_t af0 [BATCH_SIZE * HIDDENNO];
+fix16_t in_data[BATCH_SIZE][INPUTNO];
+fix16_t af1 [BATCH_SIZE * OUTPUTNO];
+fix16_t rel0[BATCH_SIZE * HIDDENNO];
+fix16_t rel1[BATCH_SIZE * OUTPUTNO];
+       
+
 void TestNetwork (const int input_size,
                                  const int output_size,
                                  const int hidden_size,

するとやはり、L1キャッシュには収まりきらずリンク時にエラーが発生する。とりあえず動作させるためにできることと言えば、バッチサイズを減らしてデータサイズを減らすことなので、バッチサイズを1まで減らすとリンクまで成功した。

一応クラッシュしないようになったが、まだデータを入力していないので、そこは修正していかないといけない。 そしてよく考えると各行列ステージで別々に変数を用意する必要もないわけで、もうちょっとデータサイズ減らせないかなあ。

/home/msyksphinz/work/freedom-e-sdk/work/build/riscv-gnu-toolchain/riscv64-unknown-elf/prefix/lib/gcc/riscv64-unknown-elf/7.1.1/../../../../riscv64-unknown-elf/bin/ld: mnist セクション `.bss' は領域 `ram' 内に入りません
/home/msyksphinz/work/freedom-e-sdk/work/build/riscv-gnu-toolchain/riscv64-unknown-elf/prefix/lib/gcc/riscv64-unknown-elf/7.1.1/../../../../riscv64-unknown-elf/bin/ld: section .stack VMA [0000000080003800,0000000080003fff] overlaps section .bss VMA [0000000080000a48,0000000080009817]
/home/msyksphinz/work/freedom-e-sdk/work/build/riscv-gnu-toolchain/riscv64-unknown-elf/prefix/lib/gcc/riscv64-unknown-elf/7.1.1/../../../../riscv64-unknown-elf/bin/ld: 領域 `ram' が 0 バイト溢れました
collect2: error: ld returned 1 exit status
/home/msyksphinz/work/freedom-e-sdk/bsp/env/common.mk:44: ターゲット 'mnist' のレシピで失敗しました
make[1]: *** [mnist] エラー 1
make[1]: ディレクトリ '/home/msyksphinz/work/freedom-e-sdk/software/mnist' から出ます
Makefile:199: ターゲット 'software' のレシピで失敗しました
make: *** [software] エラー 2

f:id:msyksphinz:20170824011043p:plain

LLVM for RISC-Vのステータスアップデート

f:id:msyksphinz:20170822223716p:plain

RISC-Vのコンパイラとしては主にGCCが提供されており、LLVMはいつからか開発が停止していた。

ところがここ数日でLLVMに関してアップデートがあったようで、どうやら開発はまだ継続している様子。

github.com

[llvm-dev] RISC-V LLVM status update

本家の最新に近いリビジョン(rL311271)に対するパッチとして開発されているようだ。

メインプログラマー(?)のAlex Bradbury氏によると、

  • 2-3週間のうちに落ちているリグレッションテスト, ABIコンプライアンステストをパスさせ、MCレイヤバックエンドを実装する。
  • 10月末までに、Clangツールチェインドライバ、初期のMAFD MCレイヤ+codegen, 呼び出し規約を実装する。初期のベンチマークプログラムをRISC-V GCCの比較として評価し、生成コードの品質を向上させる。
  • 12月末までに、MAFDのサポートを完了させ、MAFDのコンプライアンステストとより大規模だcodegenのテストを実行する。さらにバックエンドのドキュメントを拡張し、生成コードの品質を向上させる。

ということだ。とりあえずはマイルストーンも設定されているようだし、LLVM for RISC-Vの開発が進んでいるようで良かった。 とりあえず進捗に注目かな。

RISC-VプロセッサHiFive1で機械学習コードを動作させる(2. ニューラルネットのパラメータのロード)

f:id:msyksphinz:20170821013230p:plain

MNISTのデータをロードするところまでできるようになった。まずは学習処理ではなく、学習結果のパラメータをロードしてデータを評価できるようにする。

学習済みデータをオブジェクトファイルに変換する

前回と同様、パラメータなどの初期値データはファイルからロードできないので、オブジェクトファイルに変換してバイナリファイルに埋め込んでしまう。

そのために、学習済みデータをx86で実行した結果から抽出して、RISC-Vのオブジェクトに変換してリンクする。

学習済みパラメータをバイナリファイルとして吐き出す。

  • training/machine_learning/bp/
  FILE *wh0_fp = fopen ("wh0.bin", "w");
  for (int x = 0; x < INPUTNO * HIDDENNO; x++) {
    fwrite (&wh0[x], sizeof(wh0[x]), 1, wh0_fp);
  }
  fclose (wh0_fp);

  FILE *wb0_fp = fopen ("wb0.bin", "w");
  for (int x = 0; x < HIDDENNO; x++) {
    fwrite (&wb0[x], sizeof(wb0[x]), 1, wb0_fp);
  }
  fclose (wb0_fp);

  FILE *wh1_fp = fopen ("wh1.bin", "w");
  for (int x = 0; x < HIDDENNO * OUTPUTNO; x++) {
    fwrite (&wh1[x], sizeof(wh1[x]), 1, wh1_fp);
  }
  fclose (wh1_fp);

  FILE *wb1_fp = fopen ("wb1.bin", "w");
  for (int x = 0; x < OUTPUTNO; x++) {
    fwrite (&wb1[x], sizeof(wb1[x]), 1, wb1_fp);
  }
  fclose (wb1_fp);
  wb1_fp = fopen ("wb1.txt", "w");
  for (int x = 0; x < OUTPUTNO; x++) {
    fprintf (wb1_fp, "%04x ", wb1[x]);
  }
  fclose (wb1_fp);

これで、学習済みパラメータを格納したwb0,bin, wb1.bin, wh0.bin, wh1.bin が出力される。一応、hexdump しておこう。

$ hexdump wb0.bin
0000000 01fb 0000 0635 0000 0325 0000 1289 0000
0000010 18b5 0000 f66c ffff 0153 0000 181b 0000
0000020 14fa 0000 f99b ffff 038d 0000 0436 0000
0000030 008e 0000 0ad2 0000 016e 0000 fbe5 ffff
0000040 0c11 0000 0949 0000 f787 ffff f85e ffff
0000050 ed7b ffff ff8a ffff 06e1 0000 1048 0000
0000060 fc9a ffff 1ed6 0000 023f 0000 06a2 0000
0000070 f78f ffff 05ca 0000 0cd1 0000 0a1e 0000
0000080 04ce 0000 0c55 0000 fefd ffff 098a 0000
0000090 1443 0000 00fa 0000 0b5b 0000 1454 0000
00000a0 fa8b ffff 0732 0000 0cf3 0000 f7da ffff
00000b0 0981 0000 0986 0000 0d71 0000 ec68 ffff
00000c0 08f1 0000 0344 0000
00000c8

バイナリデータをRISC-Vオブジェクトに変換し、RISC-Vバイナリにロードする

次に生成したバイナリデータをRISC-Vのオブジェクトファイルに変換し、実行ファイルにリンクする。

これはMakefileに記述しており、以下のようになる。

LDFLAGS += wb0_init.o
LDFLAGS += wb1_init.o
LDFLAGS += wh0_init.o
LDFLAGS += wh1_init.o

LINK_DEPS += wb0_init.o
LINK_DEPS += wb1_init.o
LINK_DEPS += wh0_init.o
LINK_DEPS += wh1_init.o

%_init.o: %.bin
        riscv64-unknown-elf-objcopy -I binary -O elf32-littleriscv -B riscv --rename-section .data=.rodata $< $@

wb0.binからwb0_init.oを生成していく。objcopyでRISC-Vのオブジェクトに変換んし、mnistのバイナリ生成時にリンクさせる。

読み込んだオブジェクトファイルをHiFive1ボードからダンプするプログラムを書く

実際に初期値をロードできたかどうか、ダンププログラムを書いて確認しておく。

extern char _binary_wb0_bin_start[];
extern char _binary_wb0_bin_end[];
extern char _binary_wb1_bin_start[];
extern char _binary_wb1_bin_end[];
extern char _binary_wh0_bin_start[];
extern char _binary_wh0_bin_end[];
extern char _binary_wh1_bin_start[];
extern char _binary_wh1_bin_end[];

...

  const fix16_t *wh0 = (fix16_t *)_binary_wh0_bin_start;  // [INPUTNO * HIDDENNO];
  const fix16_t *wb0 = (fix16_t *)_binary_wb0_bin_start;  // [HIDDENNO];
  const fix16_t *wh1 = (fix16_t *)_binary_wh1_bin_start;  // [HIDDENNO * OUTPUTNO];
  const fix16_t *wb1 = (fix16_t *)_binary_wb1_bin_start;  // [OUTPUTNO];

  for (i = 0; i < HIDDENNO * INPUTNO; i++) {
    fix16_t hex_value = wh0[i];
    for (int j = 7; j >=0; j--) {
      write (STDOUT_FILENO, hex_enum[(hex_value >> (j * 4)) & 0x0f], 2);
    }
    if ((i % HIDDENNO) == (HIDDENNO-1)) { write (STDOUT_FILENO, "\r\n", 2); }
  }

  for (i = 0; i < HIDDENNO; i++) {
    fix16_t hex_value = wb0[i];
    for (int j = 7; j >=0; j--) {
      write (STDOUT_FILENO, hex_enum[(hex_value >> (j * 4)) & 0x0f], 2);
    }
    if ((i % HIDDENNO) == (HIDDENNO-1)) { write (STDOUT_FILENO, "\r\n", 2); }
  }

  for (i = 0; i < HIDDENNO * OUTPUTNO; i++) {
    fix16_t hex_value = wh1[i];
    for (int j = 7; j >=0; j--) {
      write (STDOUT_FILENO, hex_enum[(hex_value >> (j * 4)) & 0x0f], 2);
    }
    if ((i % HIDDENNO) == (HIDDENNO-1)) { write (STDOUT_FILENO, "\r\n", 2); }
  }

  for (i = 0; i < OUTPUTNO; i++) {
    fix16_t hex_value = wb1[i];
    for (int j = 7; j >=0; j--) {
      write (STDOUT_FILENO, hex_enum[(hex_value >> (j * 4)) & 0x0f], 2);
    }
    if ((i % HIDDENNO) == (HIDDENNO-1)) { write (STDOUT_FILENO, "\r\n", 2); }
  }

ロードしたオブジェクトファイルは、それぞれ_binary_wh0_bin_start, _binary_wb1_bin_start, _binary_wb1_bin_start, _binary_wh1_bin_start として参照できる。これらをダンプしてUARTに出力している。

f:id:msyksphinz:20170822011522p:plain

これだけでは何が何だかさっぱりだが、データ的には想定したものがロードできた。

msyksphinz.hatenablog.com

RocketChipをカスタマイズするためのチュートリアル(4. RTLシミュレーションによる動作確認)

f:id:msyksphinz:20170821013230p:plain

前回まででRocketChipのカスタマイズと、binutilsのカスタマイズが完了した。 今回はRocketChipのシミュレーションをして動作確認してみよう。

bitrev命令のテストプログラムを作成する

新規命令のテストプログラムを作成するには、riscv-toolsの環境を使うのが便利だ。riscv-toolsは大きく分けでISAのテストをするためのisa/ディレクトリと、ベンチマークプログラムを動作させるためのbenchmarks/ディレクトリに分かれている。 ISAのテストディレクトリは様々なモード向けにちょっと複雑な構成になっているため、直感的に追加できるbenchmarksディレクトリに対してプログラムを追加した。

riscv-tools/riscv-tests/benchmarks$ tree bitrev
bitrev
└── bitrev.c

0 directories, 1 file

bitrev.cの中身は以下のように記述した。

int main (int argc, char *argv [])
{
  int x = 0x01234567;
  volatile int y;

  __asm__ ("bitrev %0, %1"
                   :"=r"(y)
                   :"r"(x)
                   :
                   );

  return 0;
}

これでbenchmarks/ディレクトリの上でmakeを実行すると、それ以外のベンチマークプログラムに加えて、bitrevのプログラムもコンパイルされる。 生成されたbitrev.riscvがバイナリファイル、bitrev.riscv.dumpがダンプファイルだ。

セクション .text.startup の逆アセンブル:

0000000080001778 <main>:
    80001778:   1141                    addi    sp,sp,-16
    8000177a:   012347b7                lui     a5,0x1234
    8000177e:   5677879b                addiw   a5,a5,1383
    80001782:   0007878b                bitrev  a5,a5
    80001786:   c63e                    sw      a5,12(sp)
    80001788:   4501                    li      a0,0
    8000178a:   0141                    addi    sp,sp,16
    8000178c:   8082                    ret
    8000178e:   1141                    addi    sp,sp,-16
    80001790:   00000517                auipc   a0,0x0
    80001794:   19050513                addi    a0,a0,400 # 80001920 <main+0x1a8>
    80001798:   e406                    sd      ra,8(sp)
    8000179a:   cc9ff0ef                jal     ra,80001462 <printstr>
    8000179e:   60a2                    ld      ra,8(sp)
    800017a0:   557d                    li      a0,-1
    800017a2:   0141                    addi    sp,sp,16
    800017a4:   8082                    ret

mainの中に、bitrev命令が入った。このバイナリファイルを指定して、RocketChipを動かしてみよう。 RocketChipの評価リポジトリemulator/に移動し、評価用のバイナリファイルをシンボリックリンクで指定する。 以下で実行可能だ。

cd emulator
mkdir output
cd output
ln -s /PATH/TO/bitrev.riscv bitrev

cd ../
make output/bitrev.out

実行が完了すると、output/bitrev.outにトレースログが出力される。確認してみよう。

C0:      61842 [1] pc=[008000164a] W[r10=0000000000000000][1] R[r 0=0000000000000000] R[r 0=0000000000000003] inst=[00000513] li      a0, 0
C0:      61843 [1] pc=[008000164c] W[r 1=0000000080001650][1] R[r 0=0000000000000000] R[r12=0000000000000003] inst=[12c000ef] jal     pc + 0x12c
C0:      61871 [1] pc=[0080001778] W[r 2=00000000800218b0][1] R[r 2=00000000800218c0] R[r16=0000000000000003] inst=[ff010113] addi    sp, sp, -16
C0:      61872 [1] pc=[008000177a] W[r15=0000000001234000][1] R[r 6=00000000800218c3] R[r18=0000000000000003] inst=[012347b7] lui     a5, 0x1234
C0:      61897 [1] pc=[008000177e] W[r15=0000000001234567][1] R[r15=0000000001234000] R[r 7=0000000000000003] inst=[5677879b] addiw   a5, a5, 1383
C0:      61898 [1] pc=[0080001782] W[r 0=00000000e6a2c480][0] R[r15=0000000001234567] R[r 0=0000000000000000] inst=[0007878b] custom0 (args unknown)
C0:      61919 [1] pc=[0080001786] W[r 0=00000000800218bc][0] R[r 2=00000000800218b0] R[r15=0000000001234567] inst=[00f12623] sw      a5, 12(sp)
C0:      61920 [1] pc=[0080001788] W[r10=0000000000000000][1] R[r 0=0000000000000000] R[r 0=0000000001234567] inst=[00000513] li      a0, 0
C0:      61921 [1] pc=[008000178a] W[r 2=00000000800218c0][1] R[r 2=00000000800218b0] R[r16=0000000001234567] inst=[01010113] addi    sp, sp, 16

0x0080001778がmainの開始位置。custom0はトレースファイルが対応していないためこのような表記になっているが、bitrev命令が実行されており、 0x01234567e6a2c480 にビット位置を反転させる命令が実行できていることが分かる。成功だ!

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

RocketChipをカスタマイズするためのチュートリアル(3. RISC-V binutilsに新規命令を追加する)

RocketChipはChiselで記述されており、改造するためにはScalaの知識が必要だ。Scalaは良く知らないので試行錯誤にはなるが、ALUに何らかの命令を追加するくらいなら何とかなりそうだ。Chiselを読み解いて、ALUに新しい演算なりなんなり、入れてみたい。

前回で、ハードウェアの部分はできたと思うので、次はソフトウェア。GCCを改造する手法について調査し、テストパタンを動作させてみる。

RISC-V binutilsの改造

Rocket Chipのリポジトリには、一緒にriscv-toolsがサブリポジトリとしてついており、これを改造しよう。

binutils(特にgas)に新しい命令を追加する方法は以下が詳しい。

Adding HW/SW support for the load and store tag instructions · lowRISC

前回も紹介したが、以下の命令を追加したい。全てのビットを逆転させる命令だ。よく考えると、これは32bit限定命令だな。

f:id:msyksphinz:20170819134638p:plain

f:id:msyksphinz:20170819134959p:plain

命令の定義

命令の定義は include/opcode/riscv-opc.hで行う。ここでは、 MATCH_BITREVMASK_BITREVを定義している。

  • MASK_xxx 命令ビットフィールドのうち、どのビット位置をデコードに使用するかをマスク形式で指定する(1がデコードに使用するビット、0がそうでないビット)
  • MATCH_xxx デコードに使用するビットの、フィールド情報を記述する。
@@ -724,6 +724,10 @@
 #define CAUSE_SUPERVISOR_ECALL 0x9
 #define CAUSE_HYPERVISOR_ECALL 0xa
 #define CAUSE_MACHINE_ECALL 0xb
+
+#define MATCH_BITREV 0x0000000B
+#define MASK_BITREV  0xfff0707f
+

f:id:msyksphinz:20170819170924p:plain

次に、命令の定義を追加する。これはDECLARE_INSNにて行えるようになってる。実態は、どうやら関数を定義しているようだ。マッチする関数を作っている。

一応、念のためCUSTOM0の命令定義を削除しておいた。命令ビットフィールド的に重なってしまうからね。

  • gdb/riscv-tdep.c
#define DECLARE_INSN(INSN_NAME, INSN_MATCH, INSN_MASK) \
static inline bool is_ ## INSN_NAME ## _insn (long insn) \
{ \
  return (insn & INSN_MASK) == INSN_MATCH; \
}
@@ -951,7 +955,7 @@ DECLARE_INSN(c_sd, MATCH_C_SD, MASK_C_SD)
 DECLARE_INSN(c_addiw, MATCH_C_ADDIW, MASK_C_ADDIW)
 DECLARE_INSN(c_ldsp, MATCH_C_LDSP, MASK_C_LDSP)
 DECLARE_INSN(c_sdsp, MATCH_C_SDSP, MASK_C_SDSP)
-DECLARE_INSN(custom0, MATCH_CUSTOM0, MASK_CUSTOM0)
+// DECLARE_INSN(custom0, MATCH_CUSTOM0, MASK_CUSTOM0)
 DECLARE_INSN(custom0_rs1, MATCH_CUSTOM0_RS1, MASK_CUSTOM0_RS1)
 DECLARE_INSN(custom0_rs1_rs2, MATCH_CUSTOM0_RS1_RS2, MASK_CUSTOM0_RS1_RS2)
 DECLARE_INSN(custom0_rd, MATCH_CUSTOM0_RD, MASK_CUSTOM0_RD)
@@ -975,6 +979,7 @@ DECLARE_INSN(custom3_rs1_rs2, MATCH_CUSTOM3_RS1_RS2, MASK_CUSTOM3_RS1_RS2)
 DECLARE_INSN(custom3_rd, MATCH_CUSTOM3_RD, MASK_CUSTOM3_RD)
 DECLARE_INSN(custom3_rd_rs1, MATCH_CUSTOM3_RD_RS1, MASK_CUSTOM3_RD_RS1)
 DECLARE_INSN(custom3_rd_rs1_rs2, MATCH_CUSTOM3_RD_RS1_RS2, MASK_CUSTOM3_RD_RS1_RS2)
+DECLARE_INSN(bitrev, MATCH_BITREV, MASK_BITREV)
 #endif
 #ifdef DECLARE_CSR
 DECLARE_CSR(fflags, CSR_FFLAGS)

命令フォーマット定義

命令フォーマットは以下のようにして定義する。opcodes/riscv-opc.c に定義を追加した。

diff --git a/opcodes/riscv-opc.c b/opcodes/riscv-opc.c
index 8343198..a06209f 100644
--- a/opcodes/riscv-opc.c
+++ b/opcodes/riscv-opc.c
@@ -623,6 +623,8 @@ const struct riscv_opcode riscv_opcodes[] =
 {"sfence.vma","I",   "s,t",  MATCH_SFENCE_VMA, MASK_SFENCE_VMA, match_opcode, 0 },
 {"wfi",       "I",   "",     MATCH_WFI, MASK_WFI, match_opcode, 0 },

+{"bitrev",    "I",   "d,s",  MATCH_BITREV, MASK_BITREV, match_opcode, 0 },
+
 /* Terminate the list.  */
 {0, 0, 0, 0, 0, 0, 0}
 };

これで、

bitrev xD, xA

のフォーマットで命令を使用することができるようになった。riscv-toolsをビルドして、この新規命令に対応したbinutilsを作成し、テストしてみよう。

新規命令のコンパイルテスト

新規命令に対応した riscv64-unknown-elf-as を作成したら、以下のテストアセンブリを作成してアセンブルしてみよう。

        .section        text
        bitrev  x1, x2

アセンブルしてみる。上手く行ったので逆アセンブルしてバイナリを見てみた。

$ riscv64-unknown-elf-as bitrev.S
$ riscv64-unknown-elf-objdump -D a.out

a.out:     ファイル形式 elf64-littleriscv


セクション text の逆アセンブル:

0000000000000000 <text>:
   0:   0001008b                bitrev  ra,sp

ちゃんと追加できていた!次は、実RTLシミュレーションで動作確認かな。

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

RocketChipをカスタマイズするためのチュートリアル(2. Chiselによるパイプラインの改造)

RocketChipはChiselで記述されており、改造するためにはScalaの知識が必要だ。Scalaは良く知らないので試行錯誤にはなるが、ALUに何らかの命令を追加するくらいなら何とかなりそうだ。Chiselを読み解いて、ALUに新しい演算なりなんなり、入れてみたい。

Rocket CoreのALU構成

Rocket CoreのALUは、Chiselでは以下のような記述になっており、まあ一応読みやすい。

class ALU(implicit p: Parameters) extends CoreModule()(p) {
  val io = new Bundle {
    val dw = Bits(INPUT, SZ_DW)
    val fn = Bits(INPUT, SZ_ALU_FN)
    val in2 = UInt(INPUT, xLen)
    val in1 = UInt(INPUT, xLen)
    val out = UInt(OUTPUT, xLen)
    val adder_out = UInt(OUTPUT, xLen)
    val cmp_out = Bool(OUTPUT)
  }

  // ADD, SUB
  val in2_inv = Mux(isSub(io.fn), ~io.in2, io.in2)
  val in1_xor_in2 = io.in1 ^ in2_inv
  io.adder_out := io.in1 + in2_inv + isSub(io.fn)
...
  // AND, OR, XOR
  val logic = Mux(io.fn === FN_XOR || io.fn === FN_OR, in1_xor_in2, UInt(0)) |
              Mux(io.fn === FN_OR || io.fn === FN_AND, io.in1 & io.in2, UInt(0))
  val shift_logic = (isCmp(io.fn) && io.cmp_out) | logic | shout
  val out = Mux(io.fn === FN_ADD || io.fn === FN_SUB, io.adder_out, shift_logic)

  io.out := out
  if (xLen > 32) {
    require(xLen == 64)
    when (io.dw === DW_32) { io.out := Cat(Fill(32, out(31)), out(31,0)) }
  }
}

まず、ALUで何を演算させるかのデコードだが、これはALUの入力であるio.fnから制御されているらしい。 このio.fnはどのような定義になっているのかというと、元を辿っていくとデコーダから出力される制御信号生成器から出力されているらしい。

    LUI->       List(Y,N,N,N,N,N,N,N,A2_IMM, A1_ZERO,IMM_U, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N),
    ADDI->      List(Y,N,N,N,N,N,N,Y,A2_IMM, A1_RS1, IMM_I, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N),
    SLTI ->     List(Y,N,N,N,N,N,N,Y,A2_IMM, A1_RS1, IMM_I, DW_XPR,FN_SLT,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N),
    SLTIU->     List(Y,N,N,N,N,N,N,Y,A2_IMM, A1_RS1, IMM_I, DW_XPR,FN_SLTU,  N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N),
    ANDI->      List(Y,N,N,N,N,N,N,Y,A2_IMM, A1_RS1, IMM_I, DW_XPR,FN_AND,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N),
    ORI->       List(Y,N,N,N,N,N,N,Y,A2_IMM, A1_RS1, IMM_I, DW_XPR,FN_OR,    N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N),
    XORI->      List(Y,N,N,N,N,N,N,Y,A2_IMM, A1_RS1, IMM_I, DW_XPR,FN_XOR,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N),
    SLLI->      List(Y,N,N,N,N,N,N,Y,A2_IMM, A1_RS1, IMM_I, DW_XPR,FN_SL,    N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N),
    SRLI->      List(Y,N,N,N,N,N,N,Y,A2_IMM, A1_RS1, IMM_I, DW_XPR,FN_SR,    N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N),

この中で、FN_xxxで記述されているのがALUの信号で、A1_xxx, A2_xxx と定義されているのが、ALUの入力値として定義されているオペランドだ。 このFN_xxxはALUで定義されているものが多い。

object ALU
{
  val SZ_ALU_FN = 4
  def FN_X    = BitPat("b????")
  def FN_ADD  = UInt(0)
  def FN_SL   = UInt(1)
  def FN_SEQ  = UInt(2)
  def FN_SNE  = UInt(3)
  def FN_XOR  = UInt(4)
  def FN_SR   = UInt(5)
  def FN_OR   = UInt(6)
...

なんとなく分かってきたので、Chiselをちょっと変更して、新規ALU命令を実行してみよう。

ALUを改造して、新規演算を実装する

実はこの実験は既に試行されている。

cgtokyo.hatenablog.com

まだチマチマとRISC-Vについて私が調査していた時代、もうChiselを触っていろいろ試行されていた。FPGAXでも発表されてご挨拶させて頂いた。

パクリといってはアレだが、同じようにBitReverseを実装してみよう。

新しくBITREV命令を定義して、ALU動作を定義する。

BITREV rD, rA

RISC-Vの命令定義フィールドは以下のようになっているので、CUSTOMのところに新しく1つ命令を追加する。

f:id:msyksphinz:20170819134638p:plain

f:id:msyksphinz:20170819134959p:plain

ALUの演算追加

ALUに新演算を追加する。まず、ALU_FNの4ビットのフィールドが全部使われているので、5ビットに拡張しておこう。

 object ALU
 {
-  val SZ_ALU_FN = 4
-  def FN_X    = BitPat("b????")
+  val SZ_ALU_FN = 5
+  def FN_X    = BitPat("b?????")
   def FN_ADD  = UInt(0)
   def FN_SL   = UInt(1)
   def FN_SEQ  = UInt(2)
@@ -26,6 +26,7 @@ object ALU
   def FN_SGE  = UInt(13)
   def FN_SLTU = UInt(14)
   def FN_SGEU = UInt(15)
+  def FN_REV  = UInt(16)

   def FN_DIV  = FN_XOR
   def FN_DIVU = FN_SR
@@ -43,6 +44,7 @@ object ALU
   def cmpUnsigned(cmd: UInt) = cmd(1)
   def cmpInverted(cmd: UInt) = cmd(0)
   def cmpEq(cmd: UInt) = !cmd(3)
+  def isRev(cmd: UInt) = cmd === FN_REV
 }
 import ALU._

@@ -87,8 +89,15 @@ class ALU(implicit p: Parameters) extends CoreModule()(p) {
   // AND, OR, XOR
   val logic = Mux(io.fn === FN_XOR || io.fn === FN_OR, in1_xor_in2, UInt(0)) |
               Mux(io.fn === FN_OR || io.fn === FN_AND, io.in1 & io.in2, UInt(0))
+  def revBit (v: Bits): UInt = Cat(
+    v( 0), v( 1), v( 2), v( 3), v( 4), v( 5), v( 6), v( 7),
+    v( 8), v( 9), v(10), v(11), v(12), v(13), v(14), v(15),
+    v(16), v(17), v(18), v(19), v(20), v(21), v(22), v(23),
+    v(24), v(25), v(26), v(27), v(28), v(29), v(30), v(31))
+  val reverse = revBit (io.in1)
   val shift_logic = (isCmp(io.fn) && io.cmp_out) | logic | shout
-  val out = Mux(io.fn === FN_ADD || io.fn === FN_SUB, io.adder_out, shift_logic)
+  val logic_rev = Mux (isRev (io.fn), reverse, shift_logic)
+  val out = Mux(io.fn === FN_ADD || io.fn === FN_SUB, io.adder_out, logic_rev)

   io.out := out
   if (xLen > 32) {

命令ビットフィールドの追加

次に、BITREV命令を追加する。CUSTOM0のところに、BITREV命令を追加した。

※ 2017/08/21 デコーダが一部間違っていたので修正しました。

diff --git a/src/main/scala/rocket/IDecode.scala b/src/main/scala/rocket/IDecode.scala
index ca8ae4a..c8d93fa 100644
--- a/src/main/scala/rocket/IDecode.scala
+++ b/src/main/scala/rocket/IDecode.scala
@@ -123,7 +123,9 @@ class IDecode(implicit val p: Parameters) extends DecodeConstants
     CSRRC->     List(Y,N,N,N,N,N,N,Y,A2_ZERO,A1_RS1, IMM_X, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.C,N,N,N,N),
     CSRRWI->    List(Y,N,N,N,N,N,N,N,A2_IMM, A1_ZERO,IMM_Z, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.W,N,N,N,N),
     CSRRSI->    List(Y,N,N,N,N,N,N,N,A2_IMM, A1_ZERO,IMM_Z, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.S,N,N,N,N),
-    CSRRCI->    List(Y,N,N,N,N,N,N,N,A2_IMM, A1_ZERO,IMM_Z, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.C,N,N,N,N))
+    CSRRCI->    List(Y,N,N,N,N,N,N,N,A2_IMM, A1_ZERO,IMM_Z, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.C,N,N,N,N),
+    BITREV->    List(Y,N,N,N,N,N,N,N,A2_X,   A1_RS1, IMM_X, DW_XPR,FN_REV,   N,M_X,        MT_X, N,N,N,N,N,N,CSR.N,N,N,N,N))
+
 }

 class SDecode(implicit val p: Parameters) extends DecodeConstants
@@ -301,7 +303,8 @@ class D64Decode(implicit val p: Parameters) extends DecodeConstants
 class RoCCDecode(implicit val p: Parameters) extends DecodeConstants
 {
   val table: Array[(BitPat, List[BitPat])] = Array(
-    CUSTOM0->           List(Y,N,Y,N,N,N,N,N,A2_ZERO,A1_RS1, IMM_X, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,N,CSR.N,N,N,N,N),
+    // CUSTOM0->           List(Y,N,Y,N,N,N,N,N,A2_ZERO,A1_RS1, IMM_X, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,N,CSR.N,N,N,N,N),
+    // BITREV->            List(Y,N,Y,N,N,N,N,N,A2_X,   A1_RS1, IMM_X, DW_XPR,FN_REV,   N,M_X,        MT_X, N,N,N,N,N,N,CSR.N,N,N,N,N),
     CUSTOM0_RS1->       List(Y,N,Y,N,N,N,N,Y,A2_ZERO,A1_RS1, IMM_X, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,N,CSR.N,N,N,N,N),
     CUSTOM0_RS1_RS2->   List(Y,N,Y,N,N,N,Y,Y,A2_ZERO,A1_RS1, IMM_X, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,N,CSR.N,N,N,N,N),
     CUSTOM0_RD->        List(Y,N,Y,N,N,N,N,N,A2_ZERO,A1_RS1, IMM_X, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N),
diff --git a/src/main/scala/rocket/Instructions.scala b/src/main/scala/rocket/Instructions.scala
index 81579f5..8020bd1 100644
--- a/src/main/scala/rocket/Instructions.scala
+++ b/src/main/scala/rocket/Instructions.scala
@@ -170,7 +170,8 @@ object Instructions {
   def FMSUB_D            = BitPat("b?????01??????????????????1000111")
   def FNMSUB_D           = BitPat("b?????01??????????????????1001011")
   def FNMADD_D           = BitPat("b?????01??????????????????1001111")
-  def CUSTOM0            = BitPat("b?????????????????000?????0001011")
+  def BITREV             = BitPat("b0000000??????????000?????0001011")
+//  def CUSTOM0            = BitPat("b?????????????????000?????0001011")
   def CUSTOM0_RS1        = BitPat("b?????????????????010?????0001011")
   def CUSTOM0_RS1_RS2    = BitPat("b?????????????????011?????0001011")
   def CUSTOM0_RD         = BitPat("b?????????????????100?????0001011")

Scalaコンパイル

一応これで実装は完了した。デバッグのために、まずはコンパイルしてコンパイルエラーが無いか確認しよう。

cd emulator
make

一応、エラー無くコンパイルが完了した。次はGCCの改造かな。

msyksphinz.hatenablog.com

RISC-VプロセッサHiFive1で機械学習コードを動作させる(1.MNISTのロード)

そろそろHiFive1ボードを活用しないと、、、

MNISTのデータは非常に巨大で、フラッシュなどに入れないとHiFive1のチップの中にはもちろん入らない。 まずはMNISTデータを小さくしてオブジェクトとして貼り付け、HiFive1のシリアルコンソールから出力するところから始めよう。

MNISTデータを小さくしてオブジェクトにする

前回も紹介したが、MNISTデータは巨大だし、組み込み向けボードはファイルI/Oという概念がないので、「ファイルを開いてロードする」という手法は使えない。 従って、ファイルは予めRISC-V互換のオブジェクトファイルに変換しておき、それを実行ファイルにリンクしておくということでデータを読み込む。

普通のやつらの下を行け: objcopy で実行ファイルにデータを埋め込む - bkブログ

msyksphinz.hatenablog.com

まずは、train-images-idx3-ubyteのデータから先頭を切り取って小さくする。次に、objcopyを使ってこれをオブジェクトに変換するというわけだ。

train-images-idx3-ubyte.100.o: train-images-idx3-ubyte
    head -c 4000000 $< > $<.100
    riscv64-unknown-elf-objcopy -I binary -O elf32-littleriscv -B riscv --rename-section .data=.rodata $<.100 $@


%-ubyte.o: %-ubyte
    $(OBJCOPY) -I binary -O elf32-littleriscv -B riscv --rename-section .data=.rodata $(notdir $<) $(notdir $@)

一応hexdumpでファイルの中身を確認してみる。

f:id:msyksphinz:20170819024907p:plain

これがちゃんとオブジェクトファイルに変換されていればよい。

MNISTデータを埋め込んでシリアルコンソールに表示する

github.com

MNISTは以下のように埋め込んでいる。これを1文字ずつ変換してシリアルコンソールに表示する。write()関数は、HiFive1のシリアルコンソールに出力させる。

const char* hex_enum[] = {"0", "1", "2", "3", "4", "5", "6", "7",
                          "8", "9", "a", "b", "c", "d", "e", "f"};

int main ()
{
  write (STDOUT_FILENO, message, strlen (message));

  int len = _binary_train_images_idx3_ubyte_100_end - _binary_train_images_idx3_ubyte_100_start;
  
  int i;
  const int offset = 0x12;
  for (i = 0; i < len; i++) {
    char hex_value = _binary_train_images_idx3_ubyte_100_start[i+offset];
    
    write (STDOUT_FILENO, hex_enum[(hex_value >> 4) & 0x0f], 2); 
    write (STDOUT_FILENO, hex_enum[(hex_value >> 0) & 0x0f], 2);

    if ((i % 28) == 27) { write (STDOUT_FILENO, "\r\n", 2); }
  }

これで生成したバイナリファイルをHiFive1に流し込んで、実行してみる。

f:id:msyksphinz:20170819024526p:plain

無事にMNISTのデータを表示させることができた。