FPGA開発日記

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

「GPUを支える技術」読む (第1章)

Hisa Ando氏の著書「GPUを支える技術」を買っていたのだが、ずいぶんと積ん読にしているのだった。

なので、一応最後まで読んでいきたい。こういうのは、きちんと宣言しないと途中で辞めちゃうので宣言する。頑張って最後まで読んでいこう。

一応、Jupyter Notebookでまとめを作っていく。画像などはコピーではなく、すべて自分で作成している。

これを最後までできるかな?頑張る。

https://zhhqrw.bn1304.livefilestore.com/y4m0LeEY6uHe38Rx39YNIrxDeH4IAFz_aGW0AMUAwiX8uRQl8G4RMEv3qh0Jhao6UFV7kjKU4vQvBgkIO68sjFp-W0b0FrFvIzbPgWFH--hCVgUAFzcUDUOPD5A_rJkndBrs4igCIqAwOw-O0wAM9x27zQ5oogLVaKILztmqMtGG_VRrFmq7u2ZS8AClFZJr-B0f6Blq2gLEr-q9rEt6gt0xQ?width=1423&height=2991&cropmode=none

GPUを支える技術 ――超並列ハードウェアの快進撃[技術基礎] (WEB+DB PRESS plus)

GPUを支える技術 ――超並列ハードウェアの快進撃[技術基礎] (WEB+DB PRESS plus)

f:id:msyksphinz:20171105011618p:plain

プログラミング言語Rustに入門中 (goblinを使ってelfファイルを解析する2.)

f:id:msyksphinz:20171103203951p:plain

RustのライブラリであるGoblinを使って、バイナリファイルを解析のプログラムを作成している。Rustの構造はなかなか慣れないので、進めるのが大変だ。

Goblinを使った場合、Sectionを一つずつ解析して、バイナリの入っている部分を取り出していく必要がある。

github.com

for section in &elf.section_headers {
    println!("elf.section_headers = {:#?}, file_offset = {:#x}, size = {:#x}",
             &shdr_strtab[section.sh_name],
             section.sh_offset,
             section.sh_size
    );
    for idx in 0..section.sh_size {
        let mut offset = idx+section.sh_offset;
        print!("{:02x}", buffer[offset as usize]);
        if idx % 4 == 3 {
            print!("\n");
        }
    }
}

上記のようにして、elfのバッファ(そのままelfファイルをバイナリとして配列で格納したもの)があるので、各セクションのヘッダ部分をオフセットで計算する。それがsection.sh_offsetだ。

このような実装で、とりあえず各セクションからバイナリの情報を引き出すプログラムを構築した。

cargo run ~/work/rocket-chip-msyksphinz/riscv-tools/riscv-tests/benchmarks/qsort.riscv

elf.section_headers = "", file_offset = 0x0, size = 0x0
elf.section_headers = ".text.init", file_offset = 0x1000, size = 0x1c5
81400141
81410142
81420143
81430144
81440145
81450146
...
4e7fee7f
51617300
20300000
00elf.section_headers = ".tohost", file_offset = 0x2000, size = 0x48
00000000
00000000
00000000
...
00000000
00000000
00000000
elf.section_headers = ".text", file_offset = 0x2048, size = 0x8c0
aa871737
00001307
e7ae0145
0c4303a8
...
elf.symbol = "" 0x80005b38
elf.symbol = "" 0x80005b80
elf.symbol = "" 0x0
elf.symbol = "/tmp/cc9WJ5Mg.o" 0x0
elf.symbol = "trap_entry" 0x80000120
elf.symbol = "qsort_main.c" 0x0
elf.symbol = "verify.constprop.1" 0x80001048
elf.symbol = "syscalls.c" 0x0
elf.symbol = "vprintfmt" 0x800011ba

プログラミング言語Rustに入門中 (goblinを使ってelfファイルを解析する)

f:id:msyksphinz:20171103203951p:plain

Rustを使うなら何を作ってみたいって、並列性をうまく利用してメニーコアのシミュレータとかを作るのが、CPUアーキテクチャの勉強にとっても、Rustの勉強にとっても良さそうだ。

でも、RustでElfファイルを扱う方法ってあるのか?いろいろと調べていると、ライブラリはあるらしい。 今回調査したのは goblin というライブラリで、 elfファイルをParseしてくれるらしい。

goblin 0.0.12 - Docs.rs

いくつかこれを使ったサンプルプログラムが存在しているが、RustはVerilogやChiselと同じく、ほとんど情報が無い。少し流行っているとはいえ、やはり情報収集は大変だなあ。。。

バイナリファイルを読み込んで、シンボル情報を読み込む

goblinというライブラリを使うと、バイナリファイルを解析することが出来る。試しにRustでプログラムを書いてみた。

github.com

            match Object::parse(&buffer)? {
                Object::Elf(elf) => {
                    // println!("elf: {:#?}", &elf);
                    for header in &elf.program_headers {
                        println!("elf.program_headers = {:#?}", header);
                    }
                    let shdr_strtab = &elf.shdr_strtab;
                    for section in &elf.section_headers {
                        println!("elf.section_headers = {:#?}", &shdr_strtab[section.sh_name]);
                    }
                    let sym_strtab = &elf.strtab;
                    for symbol in &elf.syms {
                        println!("elf.symbol = {:#?} {:#x}", &sym_strtab[symbol.st_name], symbol.st_value);
                    }
                }

情報が無さ過ぎて大変なので、githubでいくつか探すと、以下のサンプルプログラムが出てきた。

github.com

elfの情報からセクションヘッダとシンボルテーブルを読み込んでprintln()で出力している。 symbol.st_namesection.sh_nameはシンボルテーブルに対するインデックスに過ぎないので、配列を参照してシンボル情報を出力する。

$ cargo run ~/work/rocket-chip-msyksphinz/riscv-tools/riscv-tests/benchmarks/qsort.riscv
...
elf.symbol = "_tls_data" 0x80005b80
elf.symbol = "abort" 0x800015c6
elf.symbol = "_init" 0x80001774
elf.symbol = "setStats" 0x8000154e
elf.symbol = "strnlen" 0x80001850
elf.symbol = "sort" 0x80001086
elf.symbol = "_start" 0x80000000
elf.symbol = "memset" 0x8000172e
elf.symbol = "main" 0x80001908
elf.symbol = "strcmp" 0x80001872
elf.symbol = "sprintf" 0x800016ac
elf.symbol = "printhex" 0x80001634
elf.symbol = "_tdata_end" 0x0
elf.symbol = "_end" 0x80005b80
elf.symbol = "fromhost" 0x80001040
elf.symbol = "_tdata_begin" 0x0
elf.symbol = "tohost" 0x80001000
elf.symbol = "exit" 0x800015be
elf.symbol = "tohost_exit" 0x8000159c
elf.symbol = "verify_data" 0x80001b38
elf.symbol = "_tbss_end" 0x44
elf.symbol = "strlen" 0x80001838
elf.symbol = "thread_entry" 0x8000162e

MicroPython試行(3. RISC-V移植環境の調査 → Spikeでの動作)

MicroPythonをRISC-Vのプラットフォームで動作させたくて、調査している。

MicroPythonはQEMUの環境も用意されていて、ARMやXtensaなどの環境も用意されている。これらを参考にしながら、実装を進めていこう。

RISC-V用に用意しなければならないのは、

  • nlr_push()
  • nlr_jump()

の関数だ。これらについて日本語の資料を探したのだが見つからないので、とりあえずARM版、Xtensa版を見ながら見様見真似で進めている。

どうやら、Calling Conventionに基づいてレジスタの情報をバッファに突っ込むような作業が必要になる。

などを保管しておく必要があるが、基本的にCalling ConventionにおけるCallee側を保存して置けばよいという理解だ。

// We only need the functions here if we are on arm/thumb, and we are not
// using setjmp/longjmp.
//
// For reference, arm/thumb callee save regs are:
//      r4-r11, r13=sp

なるほど、つまりRISC-Vであれば、Callee Save側のレジスタを保存すればよいのかしら。ここらへん実際にアプリがどのように動くのか、さっぱりわからん。

f:id:msyksphinz:20171102014057p:plain

というわけで一生懸命アセンブリを書いた。しかも、あとでよく見てみるとCaller Save側のReturn Addressも保存しなければならないのか。

unsigned int nlr_push(nlr_buf_t *nlr) {

    __asm volatile (
    "sw  x2,  8 (x10) \n" // save regs...
    "sw  x8,  12(x10) \n"
    "sw  x9,  16(x10) \n"
    "sw  x18, 20(x10) \n"
    "sw  x19, 24(x10) \n"
    "sw  x20, 28(x10) \n"
    "sw  x21, 32(x10) \n"
    "sw  x22, 36(x10) \n"
    "sw  x23, 40(x10) \n"
    "sw  x24, 44(x10) \n"
    "sw  x25, 48(x10) \n"
    "sw  x26, 52(x10) \n"
    "sw  x27, 56(x10) \n"
    "sw  ra,  60(x10) \n"
    "j   nlr_push_tail \n" // do the rest in C
    );

    return 0; // needed to silence compiler warning
}

...

NORETURN void nlr_jump(void *val) {
    nlr_buf_t **top_ptr = &MP_STATE_THREAD(nlr_top);
    nlr_buf_t *top = *top_ptr;
    if (top == NULL) {
        nlr_jump_fail(val);
    }

    top->ret_val = val;
    *top_ptr = top->prev;

    __asm volatile (
    "lw  sp,  8 (%0) \n" // restore regs...
    "lw  s0,  12(%0) \n"
    "lw  s1,  16(%0) \n"
    "lw  s2,  20(%0) \n"
    "lw  s3,  24(%0) \n"
    "lw  s4,  28(%0) \n"
    "lw  s5,  32(%0) \n"
    "lw  s6,  36(%0) \n"
    "lw  s7,  40(%0) \n"
    "lw  s8,  44(%0) \n"
    "lw  s9,  48(%0) \n"
    "lw  s10, 52(%0) \n"
    "lw  s11, 56(%0) \n"
    "lw  ra,  60(%0) \n"
    "li  a0, 1       \n" // return 1, non-local return
    "ret             \n" // return
    :                               // output operands
    : "r"(top)                      // input operands
    :                               // clobbered registers
    );

    for (;;); // needed to silence compiler warning
}

これで動かしてみる。やってみるのはSpikeシミュレータでの動作だ。とりあえずこれで動作を確認したい。

f:id:msyksphinz:20171102013920p:plain

やった!動作した。とりあえず方針としては合っているようだ。 次は、これをRocket Chipとか、HiFive1で動かしてみたいな。

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

RISC-Vの命令セットシミュレータSpikeでRoCCインタフェースを実装する2.

f:id:msyksphinz:20170201002310j:plain

UCBの開発しているシミュレータSpikeは、RISC-Vの命令セットをシミュレーションすることのできるISSで、サイクル精度を出すことはできないが最初のアプリケーションのデバッグに有用なツールだ。

そして、RISC-Vにはカスタム命令 (custom0 - custom3)が用意されており、Rocket Chipの場合にはこの命令フィールドを用いてRoCCインタフェース上にアクセラレータを搭載することが出来る。

どうせ元はChiselなのだから、この辺をうまくマージすることが出来ないだろうかといろいろ調査していたのだが、どうやらRoCCインタフェースをSpikeシミュレータではシミュレーションすることが出来るようだ。

今回はmemtotal_rocc.cc を作成してディレクトリを作成しビルドした。32ビット整数のDotProductを計算する回路(Chiselで作成したもの)と同一のものをビルド作成した。

  • riscv-tools/riscv-isa-sim/memtotal_rocc/memtotal_rocc.cc
class memtotal_rocc_t : public rocc_t
{
 public:
  const char* name() { return "memtotal_rocc"; }

  reg_t custom0(rocc_insn_t insn, reg_t xs1, reg_t xs2)
  {
    reg_t total = 0;
    switch (insn.funct)
    {
    case 0:
      c0_reg_lengthM = xs1;
      break;
    case 1:
      c0_reg_lengthK = xs1;
      break;
    case 2: {
      reg_t total_lo = 0, total_hi = 0;
      reg_t xs1_p = xs1, xs2_p = xs2;
      for (reg_t i = 0; i < c0_reg_lengthM; i++) {
        total_lo += p->get_mmu()->load_int32(xs1_p) * p->get_mmu()->load_int32(xs2_p);
        xs1_p += sizeof(int32_t);
        xs2_p += c0_reg_lengthK * sizeof(int32_t);
      }
      xs1_p = xs1;
      xs2_p = xs2 + sizeof(int32_t);
      for (reg_t i = 0; i < c0_reg_lengthM; i++) {
        total_hi += p->get_mmu()->load_int32(xs1_p) * p->get_mmu()->load_int32(xs2_p);
        xs1 += sizeof(int32_t);
        xs2 += c0_reg_lengthK * sizeof(int32_t);
      }
      total = ((total_hi & 0x0ffffffffUL) << 32) | (total_lo & 0x0ffffffffUL);
      break;
    }
    }
    return total;
  }

一度build.shを実行しないとちゃんと動作しなかった。このへんはもうちょっとビルドフローを調査する必要がある。ともかく、roccインタフェースをC++で記述するとRoCCの動作をC++で記述することが出来る。

拡張として memtotal_roccのライブラリを指定するとちゃんとRoCCアクセラレータの実装をSpikeで実行することが出来る。これは面白い。

./spike --extension=memtotal_rocc /home/msyksphinz/riscv64/riscv64-unknown-elf/share/riscv-tests/benchmarks/test-matrixmul32.riscv```
f:id:msyksphinz:20171101021825p:plain

追記: libmemtotal_rocc.so のビルドが可能なプロジェクトをgithubに置きました。

github.com

RISC-Vの命令セットシミュレータSpikeでRoCCインタフェースを実装する

f:id:msyksphinz:20170201002310j:plain

UCBの開発しているシミュレータSpikeは、RISC-Vの命令セットをシミュレーションすることのできるISSで、サイクル精度を出すことはできないが最初のアプリケーションのデバッグに有用なツールだ。

そして、RISC-Vにはカスタム命令 (custom0 - custom3)が用意されており、Rocket Chipの場合にはこの命令フィールドを用いてRoCCインタフェース上にアクセラレータを搭載することが出来る。

どうせ元はChiselなのだから、この辺をうまくマージすることが出来ないだろうかといろいろ調査していたのだが、どうやらRoCCインタフェースをSpikeシミュレータではシミュレーションすることが出来るようだ。

まず、サンプルプログラムとして dummy_rocc/ディレクトリにサンプルが存在している。

dummy_rocc/
├── dummy_rocc.ac
├── dummy_rocc.cc
├── dummy_rocc.mk.in
└── dummy_rocc_test.c

0 directories, 4 files

この実装 dummy_rocc.ccrocc_t を継承する形で実装されている。

  • https://github.com/riscv/riscv-isa-sim/blob/master/dummy_rocc/dummy_rocc.cc
class dummy_rocc_t : public rocc_t
{
 public:
  const char* name() { return "dummy_rocc"; }

  reg_t custom0(rocc_insn_t insn, reg_t xs1, reg_t xs2)
  {
    reg_t prev_acc = acc[insn.rs2];

    if (insn.rs2 >= num_acc)
...

これを拡張して memtotal_rocc.ccを作成してビルドを行ってみる。そのために memtotal_rocc/ ディレクトリを作成する。

$ tree memtotal_rocc/ -L 2
memtotal_rocc/
├── memtotal_rocc.ac
├── memtotal_rocc.cc
├── memtotal_rocc.mk.in
└── memtotal_rocc_test.c

0 directories, 4 files
class memtotal_rocc_t : public rocc_t
{
 public:
  const char* name() { return "memtotal_rocc"; }

...

  reg_t custom0(rocc_insn_t insn, reg_t xs1, reg_t xs2)
  {
    switch (insn.funct)
    {
      case 0:
        c0_reg_lengthM = xs1;
        break;
      case 1:
        c0_reg_lengthK = xs1;
        break;
      case 2:

        reg_t total = 0;
        for (reg_t i = 0; i < c0_reg_lengthK; i++) {
          total += p->get_mmu()->load_int32(xs1) * p->get_mmu()->load_int32(xs2);
          xs1 += sizeof(int32_t);
          xs2 += c0_reg_lengthK * sizeof(int32_t);
        }
        break;
    }
    return 0;
  }

ここで custom0 () の実装で、メモリアクセスを p->get_mmu()->load_int32(xs1) * p->get_mmu()->load_int32(xs2) としている。

さらにこの memtotal_rocc をコンパイルするようにサブモジュールとして登録する。

  • riscv-isa-sim/configure.ac
diff --git a/configure.ac b/configure.ac
index e361877..64759fa 100644
--- a/configure.ac
+++ b/configure.ac
@@ -86,7 +86,7 @@ AC_SUBST([CXXFLAGS],["-Wall -Wno-unused -g -O2 -std=c++11"])
 # The '*' suffix indicates an optional subproject. The '**' suffix
 # indicates an optional subproject which is also the name of a group.

-MCPPBS_SUBPROJECTS([ riscv, dummy_rocc, softfloat, spike_main ])
+MCPPBS_SUBPROJECTS([ riscv, dummy_rocc, memtotal_rocc, softfloat, spike_main ])

 #-------------------------------------------------------------------------
 # MCPPBS subproject groups

これでビルドするとlibmemtotal.soが作成される。

build$ ls -1 *.so
libdummy_rocc.so
libmemtotal_rocc.so
libriscv.so
libsoftfloat.so
libspike_main.so

これをオプションとして指定することで、extensionとして使用できるようになる。が、--extension=memtotal_roccで何故か認識されなかった。要解析。

$ ./spike --extension=dummy_rocc
usage: spike [host options] <target program> [target options]
Host Options:
  -p<n>                 Simulate <n> processors [default 1]
  -m<n>                 Provide <n> MiB of target memory [default 2048]
  -m<a:m,b:n,...>       Provide memory regions of size m and n bytes
                          at base addresses a and b (with 4 KiB alignment)
  -d                    Interactive debug mode
  -g                    Track histogram of PCs
  -l                    Generate a log of execution
  -h                    Print this help message
  -H                 Start halted, allowing a debugger to connect
  --isa=<name>          RISC-V ISA string [default rv32ima]
  --pc=<address>        Override ELF entry point
  --ic=<S>:<W>:<B>      Instantiate a cache model with S sets,
  --dc=<S>:<W>:<B>        W ways, and B-byte blocks (with S and
  --l2=<S>:<W>:<B>        B both powers of 2).
  --extension=<name>    Specify RoCC Extension
  --extlib=<name>       Shared library to load
  --rbb-port=<port>     Listen on <port> for remote bitbang connection
  --dump-dts  Print device tree string and exit
$ ./spike --extension=memtotal_rocc
couldn't find extension 'memtotal_rocc' (or library 'libmemtotal_rocc.so')

RISC-Vの命令セットシミュレータSpikeでもRoCCインタフェースがシミュレーションできる?

f:id:msyksphinz:20170201002310j:plain

UCBの開発しているシミュレータSpikeは、RISC-Vの命令セットをシミュレーションすることのできるISSで、サイクル精度を出すことはできないが最初のアプリケーションのデバッグに有用なツールだ。

そして、RISC-Vにはカスタム命令 (custom0 - custom3)が用意されており、Rocket Chipの場合にはこの命令フィールドを用いてRoCCインタフェース上にアクセラレータを搭載することが出来る。

どうせ元はChiselなのだから、この辺をうまくマージすることが出来ないだろうかといろいろ調査していたのだが、どうやらRoCCインタフェースをSpikeシミュレータではシミュレーションすることが出来るようだ。

github.com

riscv-isa-simのリポジトリを眺めていると、RoCCの型が定義されており、そこでダミーのRoCCを定義してシミュレーションをすることが出来る。

github.com

class extension_t
{
 public:
  virtual std::vector<insn_desc_t> get_instructions() = 0;
  virtual std::vector<disasm_insn_t*> get_disasms() = 0;
  virtual const char* name() = 0;
  virtual void reset() {};
  virtual void set_debug(bool value) {};
  virtual ~extension_t();

  void set_processor(processor_t* _p) { p = _p; }
 protected:
  processor_t* p;

  void illegal_instruction();
  void raise_interrupt();
  void clear_interrupt();
};
struct rocc_insn_t
{
  unsigned opcode : 7;
  unsigned rd : 5;
  unsigned xs2 : 1;
  unsigned xs1 : 1;
  unsigned xd : 1;
  unsigned rs1 : 5;
  unsigned rs2 : 5;
  unsigned funct : 7;
};

union rocc_insn_union_t
{
  rocc_insn_t r;
  insn_t i;
};

class rocc_t : public extension_t
{
 public:
  virtual reg_t custom0(rocc_insn_t insn, reg_t xs1, reg_t xs2);
  virtual reg_t custom1(rocc_insn_t insn, reg_t xs1, reg_t xs2);
  virtual reg_t custom2(rocc_insn_t insn, reg_t xs1, reg_t xs2);
  virtual reg_t custom3(rocc_insn_t insn, reg_t xs1, reg_t xs2);
  std::vector<insn_desc_t> get_instructions();
  std::vector<disasm_insn_t*> get_disasms();
};

例えば、riscv-isa-sim/riscv/dummy_rocc.cc にはサンプルのデザインが書いてある。これは、Rocket-ChipにおけるAccumulatorExampleのデザインそのままだね。

  reg_t custom0(rocc_insn_t insn, reg_t xs1, reg_t xs2)
  {
    reg_t prev_acc = acc[insn.rs2];

    if (insn.rs2 >= num_acc)
      illegal_instruction();

    switch (insn.funct)
    {
      case 0: // acc <- xs1
        acc[insn.rs2] = xs1;
        break;
      case 1: // xd <- acc (the only real work is the return statement below)
        break;
      case 2: // acc[rs2] <- Mem[xs1]
        acc[insn.rs2] = p->get_mmu()->load_uint64(xs1);
        break;
      case 3: // acc[rs2] <- accX + xs1
        acc[insn.rs2] += xs1;
        break;
      default:
        illegal_instruction();
    }

    return prev_acc; // in all cases, xd <- previous value of acc[rs2]
  }