FPGA開発日記

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

プロセッサのメモリコンシステンシモデルについて

プロセッサ構成はますますマルチコア化しており、同期処理や各プロセッサでの通信、データ共有をどのように行うかという問題は重要になっている。

ハードウェア・ソフトウェアには「メモリモデル」というものが定義されており、共有する変数をどのようにして取り扱うか、レーシングの状態をどのように取り扱うかというものが定義されているが、少し気になって復習がてら「ハードウェアのメモリコンシステンシとは何なのか」について調査を行ったのでメモする。

参考文献

[asin:B00SF6JN7M:detail]

高性能コンピュータ技術の基礎

高性能コンピュータ技術の基礎

yohhoy.hatenablog.jp

メモリコンシステンシモデルが必要な状況

例えば、以下のような状況を考えたとき、2つのプロセッサはどのような動作をするだろうか。

f:id:msyksphinz:20180615020750p:plain

キャッシュの存在しない状態で、メモリの書き込みとメモリの読み込みがプログラムの順番通りに行われる場合、最後のB==0は不成立で、A==0も不成立となるだろう。

しかし、キャッシュが存在しておりキャッシュの中身をWriteInvalidateするようなケースでは時間軸が互いにずれてしまい、上記のように互いに必ず不成立になるという状態はありえなくなる。 また、プロセッサの高速化によりMEM[B]の読み込み動作がMEM[A]の書き込み動作を追い越すような場合、上記の前提は成立しなくなる。

厳密にメモリアクセスの順序を守ればいいのではないか?

基本的にはその通り。この、すべてのメモリアクセスの順番を守るメモリモデルのことを「シーケンシャルコンシステンシ」と呼ぶ。

しかしこのシーケンシャルコンシステンシに基づいているアーキテクチャでは、厳密にメモリアクセスの順番を守るために非常に性能が落ちるという問題がある。

そこで、ある程度であればメモリアクセスの順番を入れ替えてもよい、というのが、シーケンシャルコンシステンシ以外のメモリコンシステンシモデルである。

誰がメモリコンシステンシを守るのか

プロセッサの種類(プロセッサーアーキテクチャ)によって守るべきメモリコンシステンシモデルは異なっている。 例えば、ARMやRISC-Vはほとんどメモリアクセスの順番を入れ替えてもよい仕様になっているが、SPARCは一定のメモリアクセスの順番を守らなければならない。

また、それぞれのアーキテクチャ向けのコンパイラは、個々のアーキテクチャのメモリモデルに応じてアセンブリを出力する必要がある。

制約を緩和したメモリコンシステンシモデル(リラックスコンシステンシモデル)

というわけで、シーケンシャルコンシステンシでは十分に性能を出すことができないため、もう少し制約を緩和してもいいんじゃないの?というメモリモデルが存在する。 それらについて紹介するとともに、代表的なプロセッサアーキテクチャを列挙していく。

ここでは、Write(W)→Read(R)という表現を使い、W→Rは、Wが実行された後にRが実行されなければならない、ということを意味する。

  1. Total Store Ordering(TSO)もしくはプロセッサコンシステンシ : シーケンシャルコンシステンシに対して、W→Rのオーダリングを許可する。つまり、アセンブリの順番的にストア命令の後にロード命令が配置されているものを、順番を入れ替えてロード命令を先に発行してもよいという意味である。
  2. Partial Store Ordering(PSO) : シーケンシャルコンシステンシに対して、W→R, W→Wのオーダリングを許可する。つまり、アセンブリの順番的に2つ並んだストア命令を、逆の順序にメモリアクセスを行ってもよい、という意味である (ヘネパタ第5版は誤解を生みやすい表現なので注意!第6版が正しい)
  3. Weak Ordering : シーケンシャルコンシステンシに対して、すべての操作に対してオーダリングを許可する。
  4. Release Consistency : 考え方はWeak Orderingと同一だが、同期操作に対してオーダリングの制約が異なる。

以下はヘネパタ第6版からの流用(一部改変)。メモリコンシステンシ毎の動作の違いについて。

f:id:msyksphinz:20180615015236p:plain

こちらもヘネパタからの流用で、各プロセッサにおけるオーダリングの仕様について。

Model Used in Ordinary Ordering Synchronization Orderings
Sequential Consistency 殆どのプロセッサではオプション R→R, R→W, W→R, W→W S→W, S→R, R→S, W→S, S→S
Total Store Ordering or Processor Oreding IBMS/370, DEC VAX, SPARC R→R, R→W, W→W S→W, S→R, R→S, W→S, S→S
Partial Store Ordering SPARC R→R, R→W S→W, S→R, R→S, W→S, S→S
Weak Ordering PowerPC S→W, S→R, R→S, W→S, S→S
Release Consistency MIPS, RISC-V, Armv8, C, C++ S_A→W, S_A→R, R→S_R, W→S_R S_AS_A, S_AS_R, S_RS_A, S_RS_R

難しいのはWeak OrderingとRelease Consistencyの違いなのだが、どうも同期操作に対してRelease Consistencyの方が順序が少しだけ緩い。

ここで、"S"というのは同期操作を意味し、より厳密にはtex:S_Rtex:S_A に分類できる。 Weak Orderingでは、すべての同期操作に対して、R/WのどちらもSの前後で完全に実行を完了しなければならない。 しかし、Release Consistecyは

  • Acquire(S)後のWrite/Readの順番は守るべき(S_A→W, S_A→R)、しかし、Release(S)後のWrite/Readは守らなくてもよい(つまり、Release前に次のRead/Writeを出してもよいS_R→W, S_R→R)。
  • Read/Write後のRelease(S)の順番は守るべき(R→S_R, W→S_R)、しかし、Read/Write後のAcquire(S)の順番は守らなくてもよい(つまり、Read/Writeの前にAcquireを出してもよいR→S_A, W→S_A)。

という意味があり、Weak Orderingよりも少し制約が緩いものになっている。

RISC-V SpikeシミュレータでC/C++のprintfを実現する仕組み (4. RISC-Vのプログラムロード)

Hello Worldのプログラムを動かしながら、RISC-V Spikeシミュレータのログを追っていき、RISC-Vのブートシーケンスを追っていく、その2。 今回はRISC-Vプログラムのロード部分。

f:id:msyksphinz:20180614003059p:plain

parse_args()により関数をコールを行う。

  • riscv-pk/pk/pk.c
static size_t parse_args(arg_buf* args)
{
  long r = frontend_syscall(SYS_getmainvars, va2pa(args), sizeof(*args), 0, 0, 0, 0, 0);
  kassert(r == 0);
  uint64_t* pk_argv = &args->buf[1];
  // pk_argv[0] is the proxy kernel itself.  skip it and any flags.
  size_t pk_argc = args->buf[0], arg = 1;
  for ( ; arg < pk_argc && *(char*)(uintptr_t)pk_argv[arg] == '-'; arg++)
    handle_option((const char*)(uintptr_t)pk_argv[arg]);

  for (size_t i = 0; arg + i < pk_argc; i++)
    args->argv[i] = (char*)(uintptr_t)pk_argv[arg + i];
  return pk_argc - arg;
}

frontend_syscall()は、フロントエンドに対してシステムコールを要求する。 - riscv-pk/pk/frondend.c

long frontend_syscall(long n, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5, uint64_t a6)
{
  static volatile uint64_t magic_mem[8];

  static spinlock_t lock = SPINLOCK_INIT;
  spinlock_lock(&lock);

  magic_mem[0] = n;
  magic_mem[1] = a0;
  magic_mem[2] = a1;
  magic_mem[3] = a2;
  magic_mem[4] = a3;
  magic_mem[5] = a4;
  magic_mem[6] = a5;
  magic_mem[7] = a6;

  htif_syscall((uintptr_t)magic_mem);

  long ret = magic_mem[0];

  spinlock_unlock(&lock);
  return ret;
}

htif_syscallはフロントエンドに対してシステムコールを行う。 - riscv-pk/machine/htif.c

void htif_syscall(uintptr_t arg)
{
  do_tohost_fromhost(0, 0, arg);
}

static void do_tohost_fromhost(uintptr_t dev, uintptr_t cmd, uintptr_t data)
{
  spinlock_lock(&htif_lock);
    __set_tohost(dev, cmd, data);

    while (1) {
      uint64_t fh = fromhost;
      if (fh) {
        if (FROMHOST_DEV(fh) == dev && FROMHOST_CMD(fh) == cmd) {
          fromhost = 0;
          break;
        }
        __check_fromhost();
      }
    }
  spinlock_unlock(&htif_lock);
}

tohostfromhostpk.dmpに以下のアドレスで定義されている。 - pk.dmp

000000008000a000 <fromhost>:
000000008000a008 <tohost>:

SpikeシミュレータにおけるHTIFの扱い

riscv-fesvr/fesvr/htif.cc に、Spikeでインスタンスされるhtifの型が定義されている。 htif.ccにおいて、プログラムをロードする時にtohostfromhostのアドレスを定義している。

  • riscv-fesvr/fesvr/htif.cc
void htif_t::load_program()
{
...
  std::map<std::string, uint64_t> symbols = load_elf(path.c_str(), &mem, &entry);

  if (symbols.count("tohost") && symbols.count("fromhost")) {
    tohost_addr = symbols["tohost"];
    fromhost_addr = symbols["fromhost"];
  } else {
    fprintf(stderr, "warning: tohost and fromhost symbols not in ELF; can't communicate with target\n");
  }

load_elf()

load_elf()の実装はriscv-fesvr/fesvr/elfloader.cc に実装されている。これは特に特筆するものはなく、ひたすらelfをロードしているだけである。

  • riscv-fesvr/fesvr/elfloader.cc
std::map<std::string, uint64_t> load_elf(const char* fn, memif_t* memif, reg_t* entry)
{
  int fd = open(fn, O_RDONLY);
  struct stat s;
  assert(fd != -1);
  if (fstat(fd, &s) < 0)
    abort();
  size_t size = s.st_size;

  char* buf = (char*)mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
  assert(buf != MAP_FAILED);
  close(fd);

  assert(size >= sizeof(Elf64_Ehdr));
  const Elf64_Ehdr* eh64 = (const Elf64_Ehdr*)buf;
  assert(IS_ELF32(*eh64) || IS_ELF64(*eh64));
...

  if (IS_ELF32(*eh64))
    LOAD_ELF(Elf32_Ehdr, Elf32_Phdr, Elf32_Shdr, Elf32_Sym);
  else
    LOAD_ELF(Elf64_Ehdr, Elf64_Phdr, Elf64_Shdr, Elf64_Sym);

  munmap(buf, size);

  return symbols;
}

RISC-V SpikeシミュレータでC/C++のprintfを実現する仕組み (3. RISC-Vのブートシーケンス)

Hello Worldのプログラムを動かしながら、RISC-V Spikeシミュレータのログを追っていき、RISC-Vのブートシーケンスを追っていく。

f:id:msyksphinz:20180613232411p:plain

riscv-isa-sim/riscv/sim.cc 内のリセットベクタが最初に動作する。

  • riscv-isa-sim/riscv/sim.cc
void sim_t::make_dtb()
{
...
  uint32_t reset_vec[reset_vec_size] = {
    0x297,                                      // auipc  t0,0x0
    0x28593 + (reset_vec_size * 4 << 20),       // addi   a1, t0, &dtb
    0xf1402573,                                 // csrr   a0, mhartid
    get_core(0)->get_xlen() == 32 ?
      0x0182a283u :                             // lw     t0,24(t0)
      0x0182b283u,                              // ld     t0,24(t0)
    0x28067,                                    // jr     t0
    0,
    (uint32_t) (start_pc & 0xffffffff),
    (uint32_t) (start_pc >> 32)
  };

その直後にコンパイルしたDTB(Device Tree Blob)が挿入される。

  • riscv-isa-sim/riscv/sim.cc
void sim_t::make_dtb()
{
...
  dts = s.str();
  std::string dtb = dts_compile(dts);

  rom.insert(rom.end(), dtb.begin(), dtb.end());
  const int align = 0x1000;
  rom.resize((rom.size() + align - 1) / align * align);

ここから先はpk(Proxy Kernel)のエントリポイントに移動する。エントリポイントではまずdo_resetにジャンプする。

  • ${RISCV}/riscv64-unknown-elf/bin/pk.dmp
0000000080000000 <_ftext>:
    80000000:   1e80006f            j   800001e8 <do_reset>
  • ${RISCV}/riscv64-unknown-elf/bin/pk.dmp
00000000800001e8 <do_reset>:
    800001e8:   00000093            li  ra,0
    800001ec:   00000113            li  sp,0
    800001f0:   00000193            li  gp,0
    800001f4:   00000213            li  tp,0
...
    800002a8:   00070463            beqz    a4,800002b0 <do_reset+0xc8>
    800002ac:   6f60206f            j   800029a2 <init_first_hart>
    800002b0:   00800613            li  a2,8
    800002b4:   30461073            csrw    mie,a2

init_first_hart()にジャンプする。init_first_hart()riscv-pk/machine/minit.cに定義されている。

void init_first_hart(uintptr_t hartid, uintptr_t dtb)
{
  // Confirm console as early as possible
  query_uart(dtb);
  query_htif(dtb);

  hart_init();
  hls_init(0); // this might get called again from parse_config_string
...

init_first_hart()での仕事

init_first_hart()は以下の関数が並んでいる。

  • query_uart(dtb)

    • fdt_scan()を呼び出して設定する。

    • そもそもFDT(Flatten Device Tree)とは。

      • Device Treeのバイナリ表現がFDT。

      • fdt_cb構造体の表現

      • riscv-pk/machine/fdt.h

      struct fdt_cb {
        void (*open)(const struct fdt_scan_node *node, void *extra);
        void (*prop)(const struct fdt_scan_prop *prop, void *extra);
        void (*done)(const struct fdt_scan_node *node, void *extra); // last property was seen
        int  (*close)(const struct fdt_scan_node *node, void *extra); // -1 => delete the node + children
        void *extra;
      };
    void query_uart(uintptr_t fdt)
    {
      struct fdt_cb cb;
      struct uart_scan scan;
    
      memset(&cb, 0, sizeof(cb));
      cb.open = uart_open;
      cb.prop = uart_prop;
      cb.done = uart_done;
      cb.extra = &scan;
    
      fdt_scan(fdt, &cb);
    }
  • query_htif(dtb)

    static void htif_done(const struct fdt_scan_node *node, void *extra)
    {
      struct htif_scan *scan = (struct htif_scan *)extra;
      if (!scan->compat) return;
    
      htif = 1;
    }
  • hart_init()

    • hartの初期化を行う。具体的には以下の通り。

      • minit.c
    static void hart_init()
    {
      mstatus_init();
      fp_init();
      delegate_traps();
    }
  • fp_init():minit.c
      static void fp_init()
      {
        if (!supports_extension('D') && !supports_extension('F'))
          return;
      
        assert(read_csr(mstatus) & MSTATUS_FS);
      
      #ifdef __riscv_flen
        for (int i = 0; i < 32; i++)
          init_fp_reg(i);
        write_csr(fcsr, 0);
      #else
        uintptr_t fd_mask = (1 << ('F' - 'A')) | (1 << ('D' - 'A'));
        clear_csr(misa, fd_mask);
        assert(!(read_csr(misa) & fd_mask));
      #endif
      }
  • delegate_traps():minit.c
      // send S-mode interrupts and most exceptions straight to S-mode
      static void delegate_traps()
      {
        if (!supports_extension('S'))
          return;
      
        uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;
        uintptr_t exceptions =
          (1U << CAUSE_MISALIGNED_FETCH) |
          (1U << CAUSE_FETCH_PAGE_FAULT) |
          (1U << CAUSE_BREAKPOINT) |
          (1U << CAUSE_LOAD_PAGE_FAULT) |
          (1U << CAUSE_STORE_PAGE_FAULT) |
          (1U << CAUSE_BREAKPOINT) |
          (1U << CAUSE_USER_ECALL);
      
        write_csr(mideleg, interrupts);
        write_csr(medeleg, exceptions);
        assert(read_csr(mideleg) == interrupts);
        assert(read_csr(medeleg) == exceptions);
      }
  • hls_init(0); // this might get called again from parse_config_string

    • riscv-pk/machine/minit.c
    hls_t* hls_init(uintptr_t id)
    {
      hls_t* hls = OTHER_HLS(id);
      memset(hls, 0, sizeof(*hls));
      return hls;
    }
    #define OTHER_HLS(id) ((hls_t*)((void*)HLS() + RISCV_PGSIZE * ((id) - read_const_csr(mhartid))))
  • query_finisher(dtb)

    • finisherのアドレスを記録しておくらしい。finisherはPowerOffボタンのようなものなのか?

    • riscv-pk/machine/finisher.c

    static void finisher_done(const struct fdt_scan_node *node, void *extra)
    {
      struct finisher_scan *scan = (struct finisher_scan *)extra;
      if (!scan->compat || !scan->reg || finisher) return;
      finisher = (uint32_t*)(uintptr_t)scan->reg;
    }
  • query_mem(dtb)

    • メモリの構成を記述している。

    • riscv-pk/machine/fdt.c

    static void mem_prop(const struct fdt_scan_prop *prop, void *extra)
    {
      struct mem_scan *scan = (struct mem_scan *)extra;
      if (!strcmp(prop->name, "device_type") && !strcmp((const char*)prop->value, "memory")) {
        scan->memory = 1;
      } else if (!strcmp(prop->name, "reg")) {
        scan->reg_value = prop->value;
        scan->reg_len = prop->len;
      }
    }
  • riscv-pk/machine/fdt.c 別に何かが設定された様子も無いけど、どうなっているんだろう?っていか、下記のSpikeのDTSだと0x0-0x80000000までを2回設定しているんだけど、これはいかに?
    static void mem_done(const struct fdt_scan_node *node, void *extra)
    {
      struct mem_scan *scan = (struct mem_scan *)extra;
      const uint32_t *value = scan->reg_value;
      const uint32_t *end = value + scan->reg_len/4;
      uintptr_t self = (uintptr_t)mem_done;
    
      if (!scan->memory) return;
      assert (scan->reg_value && scan->reg_len % 4 == 0);
    
      while (end - value > 0) {
        uint64_t base, size;
        value = fdt_get_address(node->parent, value, &base);
        value = fdt_get_size   (node->parent, value, &size);
        if (base <= self && self <= base + size) { mem_size = size; }
      }
      assert (end == value);
    }
  • query_harts(dtb) CPUのコンフィグレーションについて設定を行っている。"device_type=cpu"の部分のDTSを読み取っている。
    • riscv-pk/machine/fdt.c
  static void hart_prop(const struct fdt_scan_prop *prop, void *extra)
  {
  struct hart_scan *scan = (struct hart_scan *)extra;
  if (!strcmp(prop->name, "device_type") && !strcmp((const char*)prop->value, "cpu")) {
    assert (!scan->cpu);
    scan->cpu = prop->node;
  } else if (!strcmp(prop->name, "interrupt-controller")) {
    assert (!scan->controller);
    scan->controller = prop->node;
  } else if (!strcmp(prop->name, "#interrupt-cells")) {
    scan->cells = bswap(prop->value[0]);
  } else if (!strcmp(prop->name, "phandle")) {
    scan->phandle = bswap(prop->value[0]);
  } else if (!strcmp(prop->name, "reg")) {
    uint64_t reg;
    fdt_get_address(prop->node->parent, prop->value, &reg);
    scan->hart = reg;
  }
  }
static void hart_done(const struct fdt_scan_node *node, void *extra)
{
  struct hart_scan *scan = (struct hart_scan *)extra;

  if (scan->cpu == node) {
    assert (scan->hart >= 0);
  }

  if (scan->controller == node && scan->cpu) {
    assert (scan->phandle > 0);
    assert (scan->cells == 1);

    if (scan->hart < MAX_HARTS) {
      hart_phandles[scan->hart] = scan->phandle;
      hart_mask |= 1 << scan->hart;
      hls_init(scan->hart);
    }
  }
}
  • query_clint(dtb)

  • query_plic(dtb)

  • wake_harts() CPUを起動する。はずなのだが、HLSというのは何なのか良く分からない。

  static void wake_harts()
  {
  for (int hart = 0; hart < MAX_HARTS; ++hart)
    if ((((~disabled_hart_mask & hart_mask) >> hart) & 1))
      *OTHER_HLS(hart)->ipi = 1; // wakeup the hart
  }
  • plic_init()

  • hart_plic_init()

  • //prci_test()

  • memory_init() メモリの初期化なのだが、実際にはメモリサイズの設定をしているように見える。

    • riscv-pk/machine/minit.c
  static void memory_init()
  {
  mem_size = mem_size / MEGAPAGE_SIZE * MEGAPAGE_SIZE;
  }
  • boot_loader(dtb) boot_loader()riscv-pk/pk/pk.cに定義されている。riscv-pk/bbl/bbl.cにも定義されているがこれは別物らしい。オブジェクトをダンプしてみると中身が異なっていた。

    • riscv-pk/pk/pk.c
void boot_loader(uintptr_t dtb)
{
  extern char trap_entry;
  write_csr(stvec, &trap_entry);
  write_csr(sscratch, 0);
  write_csr(sie, 0);
  set_csr(sstatus, SSTATUS_SUM);
  
  file_init();
  enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0);
}
  • file_init() stdin, stdout, stderr を作成する。files_tという型名でグローバル領域に領域が確保されている。file_tの型は以下の通り。
    • riscv-pk/pk/file.h
typedef struct file
{
  int kfd; // file descriptor on the host side of the HTIF
  uint32_t refcnt;
} file_t;
  • riscv-pk/pk/file.c : file_get_free()参照カウントがまだ0になっている構造体を取得する。dupはインクリメントの回数を増やす?
static file_t* file_get_free()
{
  for (file_t* f = files; f < files + MAX_FILES; f++)
    if (atomic_read(&f->refcnt) == 0 && atomic_cas(&f->refcnt, 0, 2) == 0)
      return f;
  return NULL;
}
int file_dup(file_t* f)
{
  for (int i = 0; i < MAX_FDS; i++)
  {
    if (atomic_cas(&fds[i], 0, f) == 0)
    {
      file_incref(f);
      return i;
    }
  }
  return -1;
}
  • enter_supervisor_mode()ではプログラムをロードして実行するのだが、mepcに関数の先頭アドレスをロードして、mretで実行する。 enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0) としており、rest_of_boot_loader()の内容についてみていく。
    • riscv-pk/pk/pk.c
    static void rest_of_boot_loader(uintptr_t kstack_top)
    {
      arg_buf args;
      size_t argc = parse_args(&args);
      if (!argc)
    panic("tell me what ELF to load!");
  // load program named by argv[0]
  long phdrs[128];
  current.phdr = (uintptr_t)phdrs;
  current.phdr_size = sizeof(phdrs);
  load_elf(args.argv[0], &current);

  run_loaded_program(argc, args.argv, kstack_top);
}

fdt_scan()の実装

Flatten Device Tree の構造しかParseしない。

まずはFTDのヘッダを見ていくと、構造体としては以下のように定義されている。この辺りは、以下のサイトが非常に詳細に解説しており、役に立った。

http://masahir0y.blogspot.com/2014/05/device-tree.html

  • riscv-pk/machine/fdt.h
  struct fdt_header {
    uint32_t magic;
    uint32_t totalsize;
    uint32_t off_dt_struct;
    uint32_t off_dt_strings;
    uint32_t off_mem_rsvmap;
    uint32_t version;
    uint32_t last_comp_version; /* <= 17 */
    uint32_t boot_cpuid_phys;
    uint32_t size_dt_strings;
    uint32_t size_dt_struct;
  };
  • riscv-pk/machine/fdt.c
void fdt_scan(uintptr_t fdt, const struct fdt_cb *cb)
{
  struct fdt_header *header = (struct fdt_header *)fdt;

  // Only process FDT that we understand
  if (bswap(header->magic) != FDT_MAGIC ||
      bswap(header->last_comp_version) > FDT_VERSION) return;
  const char *strings = (const char *)(fdt + bswap(header->off_dt_strings));
  uint32_t *lex = (uint32_t *)(fdt + bswap(header->off_dt_struct));

  fdt_scan_helper(lex, strings, 0, cb);
}

そこから先はFDTのバイナリを読み取ってひたすらfdt_scan_helper()を実行する。

lexにはそのDevice Treeの構造を示す先頭のポインタが入り、stringにはDevice Treeの内容を示す文字列が入る。fdt_scan_helper()はデータ構造の役割を定義してあり、

  • riscv-pk/machine/fdt.h
#define FDT_BEGIN_NODE  1
#define FDT_END_NODE   2
#define FDT_PROP   3
#define FDT_NOP        4
#define FDT_END        9

となっている。たとえば、FDT_BEGIN_NODEの場合には、やたらとopen()close()をしているけどこれは何だろう。

  • riscv/pk/machine/fdt.c
static uint32_t *fdt_scan_helper(
  uint32_t *lex,
  const char *strings,
  struct fdt_scan_node *node,
  const struct fdt_cb *cb)
{
...
      case FDT_BEGIN_NODE: {
        uint32_t *lex_next;
        if (!last && node && cb->done) cb->done(node, cb->extra);
        last = 1;
        child.name = (const char *)(lex+1);
        if (cb->open) cb->open(&child, cb->extra);
        lex_next = fdt_scan_helper(
          lex + 2 + strlen(child.name)/4,
          strings, &child, cb);
        if (cb->close && cb->close(&child, cb->extra) == -1)
          while (lex != lex_next) *lex++ = bswap(FDT_NOP);
        lex = lex_next;
        break;
      }
...
}

uartの場合、それぞれ、cb->open()cb->prop()cb->done()で使用されている関数は以下となる。

  • riscv-pk/machine/uart.c
static void uart_open(const struct fdt_scan_node *node, void *extra)
{
  struct uart_scan *scan = (struct uart_scan *)extra;
  memset(scan, 0, sizeof(*scan));
}
static void uart_prop(const struct fdt_scan_prop *prop, void *extra)
{
  struct uart_scan *scan = (struct uart_scan *)extra;
  if (!strcmp(prop->name, "compatible") && !strcmp((const char*)prop->value, "sifive,uart0")) {
    scan->compat = 1;
  } else if (!strcmp(prop->name, "reg")) {
    fdt_get_address(prop->node->parent, prop->value, &scan->reg);
  }
}
static void uart_done(const struct fdt_scan_node *node, void *extra)
{
  struct uart_scan *scan = (struct uart_scan *)extra;
  if (!scan->compat || !scan->reg || uart) return;

  // Enable Rx/Tx channels
  uart = (void*)(uintptr_t)scan->reg;
  uart[UART_REG_TXCTRL] = UART_TXEN;
  uart[UART_REG_RXCTRL] = UART_RXEN;
}

prop()では、Device Tree内にUARTを示す記述が存在するかどうかをチェックしているようだ。最後にuart[...]で何かの設定をしている。uartグローバル変数として定義されている。

  • riscv-pk/machine/uart.c
  volatile uint32_t* uart;

compatibleの意味は良く分からない。regが指定してあると、割り当てられているアドレスを探索するようだ。

  • riscv-pk/machine/fdt.c
const uint32_t *fdt_get_address(const struct fdt_scan_node *node, const uint32_t *value, uint64_t *result)
{
  *result = 0;
  for (int cells = node->address_cells; cells > 0; --cells)
    *result = (*result << 32) + bswap(*value++);
  return value;
}

おまけ Proxy Kernelのグローバル変数の配置領域

  • pk.dmp
  000000008000c000 <fds>:
  000000008000c400 <magic_mem.2181>:
  000000008000d000 <stacks>:
  0000000080015000 <hart_phandles>:
  0000000080015020 <lock.2182>:
  0000000080015028 <free_pages>:
  0000000080015030 <next_free_page>:
  0000000080015038 <first_free_page>:
  0000000080015040 <vmrs>:
  0000000080015048 <vm_lock>:
  0000000080015050 <htif_lock>:
  0000000080015058 <disabled_hart_mask>:
  0000000080015060 <current>:
  00000000800150d0 <first_free_paddr>:
  00000000800150d8 <plic_ndevs>:
  00000000800150e0 <plic_priorities>:
  00000000800150e8 <mem_size>:
  00000000800150f0 <mtime>:
  00000000800150f8 <root_page_table>:
  0000000080015100 <htif>:
  0000000080015108 <htif_console_buf>:
  0000000080015110 <uart>:
  0000000080015118 <finisher>:
  0000000080015120 <hart_mask>:

おまけ Spikeが生成するDevice Tree Source

$ spike --dump-dts hoge
/dts-v1/;

/ {
  #address-cells = <2>;
  #size-cells = <2>;
  compatible = "ucbbar,spike-bare-dev";
  model = "ucbbar,spike-bare";
  cpus {
    #address-cells = <1>;
    #size-cells = <0>;
    timebase-frequency = <10000000>;
    CPU0: cpu@0 {
      device_type = "cpu";
      reg = <0>;
      status = "okay";
      compatible = "riscv";
      riscv,isa = "rv64imafdc";
      mmu-type = "riscv,sv48";
      clock-frequency = <1000000000>;
      CPU0_intc: interrupt-controller {
        #interrupt-cells = <1>;
        interrupt-controller;
        compatible = "riscv,cpu-intc";
      };
    };
  };
  memory@80000000 {
    device_type = "memory";
    reg = <0x0 0x80000000 0x0 0x80000000>;
  };
  soc {
    #address-cells = <2>;
    #size-cells = <2>;
    compatible = "ucbbar,spike-bare-soc", "simple-bus";
    ranges;
    clint@2000000 {
      compatible = "riscv,clint0";
      interrupts-extended = <&CPU0_intc 3 &CPU0_intc 7 >;
      reg = <0x0 0x2000000 0x0 0xc0000>;
    };
  };
  htif {
    compatible = "ucb,htif0";
  };
};

Elfファイルからシンボルを取り出してシミュレータでトレースを表示する機能の実装

RISC-Vシミュレータにはelfファイルを読み込ませているのだが、elfファイルにはいろんな情報が取り込まれており、例えば

  • テキスト領域の関数のヘッダアドレス
  • グローバルデータが格納されているアドレス

などの情報が格納されている。

シミュレータは、テキスト領域の命令と、データ領域の初期値をロードするのだが、それ以外にも、関数の先頭アドレスや、シンボルのアドレスをロードすることによってより多くのデバッグ情報を出力することができる。 今回、RISC-Vシミュレータに追加したのは、関数が配置されているアドレス一覧を読み込んで、プログラムがどのような順番で関数をジャンプしていったのかをトレースすることができる「サブルーチントレース」(自称)の機能だ。

f:id:msyksphinz:20180612022439p:plain
図. サブルーチントレースの実行結果。関数がどのようにしてジャンプし、どこから戻ってきたかをトレースする。このプログラムでは外部I/Oの問題でプログラムが最終的にクラッシュしている...

どのようにしてサブルーチントレースを追加するか

サブルーチントレースの機能は、主に2つに分けられている。

  • 関数ジャンプを検出する部分
  • 関数から戻ってきたことを検出する部分

関数ジャンプの検出

まず、関数ジャンプをする部分の検出は2つの手法が考えられる。

  1. ジャンプ命令を検出する。
  2. ラベル一覧と比較し、新たなラベルに到達するとそれを関数ジャンプとみなす。

  3. の方法は、アーキテクチャ毎に、どの命令が関数ジャンプなのかを登録する必要がある。これはアーキテクチャ毎の項目が多くなり汎用的ではない。 一方で2. の方法は、命令がラベル付きのアドレスに到達するとそれは関数ジャンプとみなす。命令に限定しないので、より汎用的だといえる。 一方で2. の問題点は、関数コールのつもりはないのに、ラベルを踏むとそれだけで関数コールと判定されてしまう可能性がある。 したがって、ある程度のフィルタをかけなければならず。RISCプロセッサの場合は対外関数コールに使われる命令というのは、

  4. プログラムカウンタをアップデートする。

  5. プログラムカウンタ以外のレジスタをアップデータする(戻り先アドレスの保存)

となるので、この条件を満たし、さらにラベルを踏んだ命令は関数ジャンプとみなす。

  • 実装した関数ジャンプ検出のコード。
void TraceInfo::HierFunctionCall (Addr_t fetch_pc)
{
  if (m_hier_func_in_skip == true) {
    return;
  }

  // Jump to Function

  Addr_t jump_pc;
  bool is_find_jump_pc = FindPCUpdate (&jump_pc);

  std::string func_symbol;
  if (is_find_jump_pc) {
    if (m_pe_thread->FindSymbol (jump_pc, &func_symbol) == true) {
      for (uint32_t i = 0; i < GetHierDepth (); i++) {
        fprintf (GetTraceHierFp(), "  ");
      }
      std::stringstream str;
      str << "<FunctionCall " << GetStep() << " " << func_symbol << "(0x"
          << std::hex << std::setw(8) << std::setfill('0') << jump_pc << ")";
      fprintf (GetTraceHierFp(), "%s", str.str().c_str());
      SetHierDepth (GetHierDepth()+1);
      PairStackTrace *stack_trace = new PairStackTrace();
#ifdef ARCH_RISCV
      Addr_t next_pc = fetch_pc + RiscvDec::GetInstLength (GetInstIdx()) / 8;
#endif // ARCH_RISCV
      *stack_trace = std::make_pair (next_pc, func_symbol);
      m_hier_stack.push_back (stack_trace);

      // find skip list
      for (auto it = m_hier_skip_func.begin ();
           it != m_hier_skip_func.end ();
           it++) {
        if ((*it)->first == func_symbol) {
          m_hier_func_in_skip = true;
          if ((*it)->second == InstSkip_t::InstSkip) {
            m_hier_debug_in_skip = true;
          }
          fprintf (GetTraceHierFp(), " ...");
          break;
        }
        it++;
      }
      fprintf (GetTraceHierFp(), ">\n");
    }
  }
}

関数から戻ってきたことを検出する

これは、関数ジャンプ検出時にヒントを挿入しておく。関数から戻ってくるときは、そのジャンプ命令の次の命令に戻ってくることが大半だ(そうでない場合もあるが...)。

そこで、関数ジャンプが発生した際に、「関数ジャンプが実行された命令のプログラムアドレス+関数ジャンプ命令の命令長」戻り先の候補アドレスになり、それを戻り先候補アドレスとして登録する。 そしてシミュレーション上で、戻り先候補アドレスを踏むとそれを関数から戻ってきたこととみなす。

これらの情報をもとに、トレースファイルを作成すれば、上記のように関数のジャンプ履歴を取得することができる。

  • 実装した関数から戻ってくることを検出するコード
void TraceInfo::HierReturn (Addr_t fetch_pc)
{
  // Return from Function

  if (!m_hier_stack.empty ()) {
    bool is_found_pc = false;
    int32_t pop_count = 0;
    auto trace_it = m_hier_stack.end();
    trace_it--;
    for (; trace_it != m_hier_stack.begin(); trace_it--) {
      if ((*trace_it)->first == fetch_pc) {
        is_found_pc = true;
        break;
      }
      pop_count++;
    }
    if (is_found_pc == true) {
      while (pop_count-- >= 0) {
        m_hier_stack.pop_back();
        SetHierDepth (GetHierDepth()-1);
      }
      for (uint32_t i = 0; i < GetHierDepth (); i++) {
        fprintf (GetTraceHierFp(), "  ");
      }
      fprintf (GetTraceHierFp(), "<Return: %s>\n", (*trace_it)->second.c_str());

      m_hier_func_in_skip  = false;
      m_hier_debug_in_skip = false;
    }
  }
}

RISC-V SpikeシミュレータでC/C++のprintfを実現する仕組み (2. Device Tree Blobと Proxy Kernel)

RISC-Vのシミュレータは、シミュレーション対象のプログラムのElfファイル以外に、いくつかの外部ライブラリをロードしている。

  • RISC-V の Device Tree (SpikeのビルドにDevice Tree Compiler が必要なのはこのためだ)
  • RISC-V の Proxy Kernel (I/Oなどの本来OSが行う仕事を肩代わりしている)
f:id:msyksphinz:20180611010051p:plain
図. RISC-V Spike Instruction Set Simulatorの入出力ファイル

まず、Proxy Kernelについては libpk.a ではなく ${RISCV}/riscv-unknown-elf/bin/pk を観察する。 Proxy Kernel のエントリポイントは0x8000_0000 に設定されているため、この場所からプログラムの実行がスタートする。

の前に、本当に最初に始まるのはデバッグコードの実行だ。これはSpikeに以下のように記述されている。 見てわかる通り、このコードは最初にstart_pcのコードにジャンプする。これが上記の${RISCV}/riscv-unknown-elf/bin/pkに相当する。

  • ./riscv-isa-sim/riscv/sim.cc
  uint32_t reset_vec[reset_vec_size] = {
    0x297,                                      // auipc  t0,0x0
    0x28593 + (reset_vec_size * 4 << 20),       // addi   a1, t0, &dtb
    0xf1402573,                                 // csrr   a0, mhartid
    get_core(0)->get_xlen() == 32 ?
      0x0182a283u :                             // lw     t0,24(t0)
      0x0182b283u,                              // ld     t0,24(t0)
    0x28067,                                    // jr     t0
    0,
    (uint32_t) (start_pc & 0xffffffff),
    (uint32_t) (start_pc >> 32)
  };

次にDevice Treeだが、これは何とSpikeが自身でDevice Tree Compilerを動作させて生成するようになっている。 例えば、デフォルトのSpikeを立ち上げたときに生成されるDTSは以下のようになる。これはSpike の --dump-dts で確認できる。

$ spike --dump-dts test_output_c
/dts-v1/;

/ {
  #address-cells = <2>;
  #size-cells = <2>;
  compatible = "ucbbar,spike-bare-dev";
  model = "ucbbar,spike-bare";
  cpus {
    #address-cells = <1>;
    #size-cells = <0>;
    timebase-frequency = <10000000>;
    CPU0: cpu@0 {
      device_type = "cpu";
      reg = <0>;
      status = "okay";
      compatible = "riscv";
      riscv,isa = "rv64imafdc";
      mmu-type = "riscv,sv48";
      clock-frequency = <1000000000>;
      CPU0_intc: interrupt-controller {
        #interrupt-cells = <1>;
        interrupt-controller;
        compatible = "riscv,cpu-intc";
      };
    };
  };
  memory@80000000 {
    device_type = "memory";
    reg = <0x0 0x80000000 0x0 0x80000000>;
  };
  soc {
    #address-cells = <2>;
    #size-cells = <2>;
    compatible = "ucbbar,spike-bare-soc", "simple-bus";
    ranges;
    clint@2000000 {
      compatible = "riscv,clint0";
      interrupts-extended = <&CPU0_intc 3 &CPU0_intc 7 >;
      reg = <0x0 0x2000000 0x0 0xc0000>;
    };
  };
  htif {
    compatible = "ucb,htif0";
  };
};

これをコンパイルしてDTBを作成し、これをブートコードの後ろに挿入している。

  • ./riscv-isa-sim/riscv/sim.cc
  dts = s.str();
  std::string dtb = dts_compile(dts);

  rom.insert(rom.end(), dtb.begin(), dtb.end());
  const int align = 0x1000;
  rom.resize((rom.size() + align - 1) / align * align);

  boot_rom.reset(new rom_device_t(rom));
  bus.add_device(DEFAULT_RSTVEC, boot_rom.get());

分散システムのコンセンサスアルゴリズムRaftについて調査

ビットコインの勉強の最中に、コンセンサスアルゴリズムについていろいろと調べていたのだが、じゃあ一般的に分散システムでコンセンサスってどうやってとるんだろう?ということに興味をもってコンセンサスアルゴリズムというものを知った。

最初は、ちょっと簡単そう(と言っても難しそうだが)Raftについて。

Raftの前にPaxosというアルゴリズムもあるらしいのだが、これはとても難しいらしいのでまずはRaftから。

論文とか読めばよかったのだけど、ものぐさなのでアニメーションだけ見ていた。っていうかこのページ凄いな!かなり分かりやすい。

Raft

f:id:msyksphinz:20180619235125p:plain
図. Raftについて解説するアニメーション。

Raftの論文はこちら。

  • In Search of an Understandable Consensus Algorithm (Extended Version)

https://raft.github.io/raft.pdf

Paxosについては、超絶頭の良い人たちの分かりやすい資料を見ないとさっぱりだ。

  • Paxos

www.slideshare.net

TensorFlow+Kerasに入門(3. Keras2cppでCIFAR10のモデルを変換してみる)

f:id:msyksphinz:20180701195704p:plain

FPGAの部屋のmarseeさんの記事を見て、TensorFlow+Kerasに入門してみた。 というかmarseeさんの記事で掲載されているソースコードをほとんどCopy & Pasteして実行してみているだけだが...

TensorFlow+KerasでCifar10を学習するサンプルプログラムを実行して、そこから得られたモデルを使ってKeras2cppでモデルの変換を行ってみたい。

最終的な目標は、Keras2cppを使ってC++のコードを出力し、それをネイティブC++環境で実行することだ。

まずは Kerasのサンプルコードを使用してcifar10のCNNのトレーニングを実行した。VirtualBox上のJupyter Notebook、さらにGPUを使わずに実行したので数時間はかかってしまった。 生成したモデルとトレーニングデータはファイルに保存しておくことにした。

github.com

f:id:msyksphinz:20180701193943p:plain

学習済みのモデルは、モデルファイルはJSON形式、重みパラメータはh5ファイルとして保存した。

from keras.models import load_model

model.save('cifar10_cnn_model.h5') # creates a HDF5 file 'my_model.h5'
with open('cifar10_cnn_model.json', 'w') as fout:
    fout.write(model.to_json())

保存したモデルを使って、keras2cppで動作するためのモデルに変換する。これでdumped.nnetが生成される。

$ python ./dump_to_simple_cpp.py -a ../keras_model/cifar10_cnn_model.json -w ../keras_model/cifar10_cnn_model.h5 -o dumped.nnet
Using TensorFlow backend.
Read architecture from ../keras_model/cifar10_cnn_model.json
Read weights from ../keras_model/cifar10_cnn_model.h5
Writing to dumped.nnet
2018-07-01 19:42:53.619547: I tensorflow/core/platform/cpu_feature_guard.cc:140] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2

MNISTのモデルでもそうだが、keras2cppで推論を実行するためにはテストデータを用意しなければならない。これはcifar10のデータをダウンロードして生成した。

    tr_data10, tr_labels10, te_data10, te_labels10, label_names10 = get_cifar10(datapath)

    print("3 32 32")
    print("%f. " % (te_data10[0][0] / 10.0))
    for dim in range(3):
        for y in range(32):
            print("["),
            for x in range (32):
                print("%f. " % (int(te_data10[0][dim * 32 * 32 + y * 32 + x]) / 256.0)),
            print("]")

以下のようにしてcifar10_test_data.datを生成する。

python input_cifar.py > cifar10_test_data.dat

中身は以下のようになっている。3×32×32の入力データの配列を作成した。

3 32 32
[ 0.617188.  0.621094.  0.644531.  0.648438.  0.625000.  0.609375.  0.632812.  0.621094.  0.617188.  0.621094.  0.628906.  0.625000.  0.628906.  0.648438.  0.660156.  0.664062.  0.652344.  0.632812.  0.625000.  0.625000.  0.609375.  0.582031.  0.585938.  0.578125.  0.582031.  0.558594.  0.546875.  0.550781.  0.558594.  0.535156.  0.492188.  0.453125.  ]
[ 0.593750.  0.589844.  0.621094.  0.648438.  0.632812.  0.625000.  0.640625.  0.632812.  0.636719.  0.609375.  0.605469.  0.621094.  0.636719.  0.664062.  0.667969.  0.667969.  0.660156.  0.625000.  0.601562.  0.589844.  0.566406.  0.542969.  0.546875.  0.550781.  0.582031.  0.574219.  0.566406.  0.554688.  0.558594.  0.531250.  0.488281.  0.464844.  ]
[ 0.589844.  0.589844.  0.617188.  0.652344.  0.625000.  0.636719.  0.644531.  0.644531.  0.636719.  0.632812.  0.617188.  0.613281.  0.628906.  0.648438.  0.652344.  0.660156.  0.664062.  0.621094.  0.566406.  0.472656.  0.429688.  0.382812.  0.394531.  0.445312.  0.468750.  0.523438.  0.558594.  0.546875.  0.554688.  0.542969.  0.507812.  0.468750.  ]
[ 0.605469.  0.605469.  0.625000.  0.679688.  0.652344.  0.652344.  0.660156.  0.660156.  0.644531.  0.644531.  0.652344.  0.746094.  0.691406.  0.613281.  0.632812.  0.640625.  0.617188.  0.582031.  0.406250.  0.402344.  0.382812.  0.359375.  0.312500.  0.289062.  0.335938.  0.324219.  0.441406.  0.515625.  0.546875.  0.546875.  0.531250.  0.496094.  ]
[ 0.605469.  0.609375.  0.628906.  0.664062.  0.660156.  0.636719.  0.660156.  0.648438.  0.640625.  0.640625.  0.675781.  0.960938.  0.761719.  0.589844.  0.570312.  0.554688.  0.433594.  0.304688.  0.332031.  0.441406.  0.437500.  0.414062.  0.378906.  0.363281.  0.289062.  0.328125.  0.332031.  0.410156.  0.500000.  0.539062.  0.519531.  0.503906.  ]
[ 0.578125.  0.519531.  0.507812.  0.574219.  0.628906.  0.644531.  0.652344.  0.652344.  0.636719.  0.644531.  0.636719.  0.703125.  0.613281.  0.500000.  0.378906.  0.257812.  0.269531.  0.257812.  0.347656.  0.460938.  0.476562.  0.464844.  0.445312.  0.367188.  0.386719.  0.355469.  0.226562.  0.261719.  0.421875.  0.546875.  0.539062.  0.523438.  ]
[ 0.496094.  0.425781.  0.183594.  0.343750.  0.597656.  0.664062.  0.656250.  0.664062.  0.660156.  0.648438.  0.640625.  0.574219.  0.503906.  0.496094.  0.390625.  0.265625.  0.304688.  0.281250.  0.324219.  0.515625.  0.570312.  0.484375.  0.410156.  0.417969.  0.449219.  0.332031.  0.246094.  0.179688.  0.308594.  0.515625.  0.550781.  0.523438.  ]
[ 0.511719.  0.386719.  0.164062.  0.273438.  0.558594.  0.652344.  0.644531.  0.656250.  0.667969.  0.628906.  0.546875.  0.468750.  0.507812.  0.562500.  0.453125.  0.343750.  0.355469.  0.332031.  0.300781.  0.484375.  0.636719.  0.531250.  0.398438.  0.414062.  0.390625.  0.332031.  0.210938.  0.191406.  0.222656.  0.417969.  0.539062.  0.531250.  ]
[ 0.664062.  0.402344.  0.210938.  0.484375.  0.597656.  0.628906.  0.636719.  0.648438.  0.644531.  0.679688.  0.441406.  0.488281.  0.613281.  0.609375.  0.472656.  0.335938.  0.320312.  0.328125.  0.312500.  0.316406.  0.539062.  0.570312.  0.441406.  0.339844.  0.324219.  0.335938.  0.277344.  0.218750.  0.156250.  0.289062.  0.519531.  0.535156.  ]
[ 0.703125.  0.523438.  0.367188.  0.601562.  0.679688.  0.617188.  0.609375.  0.597656.  0.808594.  0.925781.  0.808594.  0.609375.  0.679688.  0.578125.  0.488281.  0.363281.  0.335938.  0.289062.  0.230469.  0.296875.  0.535156.  0.558594.  0.519531.  0.414062.  0.335938.  0.339844.  0.328125.  0.292969.  0.195312.  0.156250.  0.371094.  0.515625.  ]
[ 0.714844.  0.421875.  0.554688.  0.644531.  0.691406.  0.605469.  0.621094.  0.476562.  0.832031.  0.925781.  0.859375.  0.640625.  0.714844.  0.609375.  0.488281.  0.468750.  0.304688.  0.312500.  0.175781.  0.355469.  0.683594.  0.613281.  0.605469.  0.417969.  0.339844.  0.402344.  0.343750.  0.304688.  0.230469.  0.160156.  0.230469.  0.406250.  ]
[ 0.734375.  0.390625.  0.527344.  0.664062.  0.730469.  0.648438.  0.675781.  0.523438.  0.457031.  0.757812.  0.777344.  0.664062.  0.722656.  0.738281.  0.523438.  0.457031.  0.398438.  0.328125.  0.148438.  0.488281.  0.820312.  0.625000.  0.570312.  0.363281.  0.324219.  0.367188.  0.406250.  0.332031.  0.285156.  0.214844.  0.242188.  0.296875.  ]
[ 0.738281.  0.351562.  0.496094.  0.683594.  0.679688.  0.648438.  0.695312.  0.621094.  0.378906.  0.656250.  0.656250.  0.535156.  0.726562.  0.843750.  0.625000.  0.480469.  0.468750.  0.449219.  0.195312.  0.585938.  0.757812.  0.605469.  0.480469.  0.355469.  0.328125.  0.328125.  0.371094.  0.335938.  0.328125.  0.285156.  0.308594.  0.285156.  ]
...

これを読み込ませてkeras2cppのサンプルコードをコンパイルし、実行する。実行するとSegmentation Faultが出てしまった。

$ g++ -g -Wall -O0 -std=c++11 keras_model.cc example_main.cc && ./a.out
This is simple example with Keras neural network model loading into C++.
Keras model will be used in C++ for prediction only.
3
Reading model from ./dumped.nnet
Layers 18
Layer 0 Conv2D
Layer is empty, maybe it is not defined? Cannot define network.
DataChunk2D 3x32x32
Segmentation fault (core dumped)

デバッグしてみると、Kerasのモデルにおいて、Convolutional2DとConv2Dの名前の違いがあるらしい?

diff --git a/keras_model.cc b/keras_model.cc
index 6c65550..1159839 100644
--- a/keras_model.cc
+++ b/keras_model.cc
@@ -420,6 +420,8 @@ void keras::KerasModel::load_weights(const string &input_fname) {
     Layer *l = 0L;
     if(layer_type == "Convolution2D") {
       l = new LayerConv2D();
+    } else if(layer_type == "Conv2D") {
+      l = new LayerConv2D();
     } else if(layer_type == "Activation") {
       l = new LayerActivation();
     } else if(layer_type == "MaxPooling2D") {

上記の修正を入れて再度実行してみる。実行結果は、、、メモリ不足?対策を考えなければ。

$ g++ -g -Wall -O0 -std=c++11 keras_model.cc example_main.cc && ./a.out
This is simple example with Keras neural network model loading into C++.
Keras model will be used in C++ for prediction only.
3
Reading model from ./dumped.nnet
Layers 18
Layer 0 Conv2D
Layer 0 Conv2D
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
Aborted (core dumped)