FPGA開発日記

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

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を取り扱った記事。

Rocket-Chipのブートシーケンスと内部構造の解析(1)

RISC-V の実装の一つである Rocket-Chip の解析を行いたいのだが、Chiselの実装が複雑で詳細の解析に阻まれており、なかなか進まない。

やりたいこととしては、現在のRocket-Chip Generatorの環境を利用せずに、Rocket-Chipの生成されたVerilogファイルだけを使用してプログラムを動作させたい。

この環境が構築できれば、Rocket-ChipのRTLだけカスタマイズして自分のオリジナルの環境が構築できるようになるだろう。 そのために、Rocket-Chipがどのように動いているのかを解析して、少なくともLinuxがブートできるような環境を構築する必要がある。

Rocket-Chipがブートするブートシーケンスの仕組みを解析する

Rocket-Chipでプログラムを動かす、例えばDhrystoneを動作させたいときはrocket-chipのリポジトリで以下のように入力する。

cd rocket-chip/emulator
make CONFIG=DefaultConfig output/dhrystone.riscv.out

これでRocket-ChipのVerilogコードが生成され、この時の実行結果を解析するためには、output/dhrystone.riscv.out を見る必要がある。 このログファイルは実際に実行されていない命令の情報も入っているので、その辺りはgrepで削除していく。

grep " \[1\] pc=" output/dhrystone.riscv.out  | less -S
C0:         21 [1] pc=[0000010040] W[r10=0000000000000000][1] R[r 0=0000000000000000] R[r20=0163790d47bd1fab] inst=[f1402573] csrr    a0, mhartid
C0:         22 [1] pc=[0000010044] W[r11=0000000000010044][1] R[r 0=0000000000000000] R[r 0=0000000000000000] inst=[00000597] auipc   a1, 0x0
C0:         23 [1] pc=[0000010048] W[r11=0000000000010080][1] R[r11=0000000000010044] R[r28=0163790d47bd1fab] inst=[03c58593] addi    a1, a1, 60
C0:         24 [1] pc=[000001004c] W[r 0=ffffffd0053e315d][0] R[r 0=0000000000000000] R[r 5=0163790d47bd1fab] inst=[10500073] wfi (args unknown)
C0:         29 [1] pc=[0000010050] W[r 0=0000000000010052][1] R[r31=150ae1ddcefdcd8f] R[r29=0163790d47bd1fab] inst=[0000bff5] j       pc - 4
C0:         30 [1] pc=[000001004c] W[r 0=ffffffd0053e315d][0] R[r 0=0000000000000000] R[r 5=0163790d47bd1fab] inst=[10500073] wfi (args unknown)
C0:         35 [1] pc=[0000010050] W[r 0=0000000000010052][1] R[r31=150ae1ddcefdcd8f] R[r29=0163790d47bd1fab] inst=[0000bff5] j       pc - 4
C0:         36 [1] pc=[000001004c] W[r 0=ffffffd0053e315d][0] R[r 0=0000000000000000] R[r 5=0163790d47bd1fab] inst=[10500073] wfi (args unknown)
C0:         41 [1] pc=[0000010050] W[r 0=0000000000010052][1] R[r31=150ae1ddcefdcd8f] R[r29=0163790d47bd1fab] inst=[0000bff5] j       pc - 4
C0:         42 [1] pc=[000001004c] W[r 0=ffffffd0053e315d][0] R[r 0=0000000000000000] R[r 5=0163790d47bd1fab] inst=[10500073] wfi (args unknown)
C0:         47 [1] pc=[0000010050] W[r 0=0000000000010052][1] R[r31=150ae1ddcefdcd8f] R[r29=0163790d47bd1fab] inst=[0000bff5] j       pc - 4
... (以下略)

デバッグ用のVCDファイルを出力しながらシミュレーションを行う

VCDファイルの出力方法は以下だ。さらに生成されたvcdファイルをwlfに変換してQuestaSimで解析する。

make CONFIG=DefaultConfig output/dhrystone.riscv.vcd
vcd2wlf output/dhrystone.riscv.vcd output/dhrystone.riscv.wlf
f:id:msyksphinz:20180108144422p:plain

これを見ていくと、Rocket-Chipのブートシーケンスは以下のように分かれているように思われる。 1. BootROMからブートコードを実行する 2. デバッグモジュールからデバッグコードをフェッチする(おそらく実行バイナリファイルから情報を引き出して、IcacheとDCacheに格納しているものと思われる)。 3. メインプログラムを実行する

ちなみに、Rocket-Chipのアドレスマップは以下のようになっている。このアドレスマップはChisel上でどのように決められているのだ?

Generated Address Map
               0 -     1000 ARWX  debug-controller@0
            3000 -     4000 ARWX  error-device@3000
           10000 -    20000  R XC rom@10000
         2000000 -  2010000 ARW   clint@2000000
         c000000 - 10000000 ARW   interrupt-controller@c000000
        60000000 - 80000000  RWX  mmio@60000000
        80000000 - 90000000  RWXC memory@80000000
f:id:msyksphinz:20180108144610p:plain

まず1.のBootROMだが、これはrocket-chipリポジトリのbootromディレクトリにコードが格納されている。

cd bootrom
...
.section .text.hang, "ax", @progbits
.globl _hang
_hang:
  csrr a0, mhartid
  la a1, _dtb
1:
  wfi
  j 1b
...

このプログラムを見ればわかるとおり、ブートシーケンスはwfi(wait for interrupt)命令によってこの割り込みが入るのを待っている。この割り込みの挿入によってデバッグコードに移っていくものと思われる。

  1. デバッグモジュールは、おそらくは実行バイナリファイルを解析してDPIと接続し、ICacheとDCacheに転送しているのではないかと思われるので、ここも要解析だ。

  2. のメインプログラムは実行しているのはすぐにわかるのだが、DCacech/ICacheの位置を見つけて解析する必要がある。

f:id:msyksphinz:20180108155301p:plain

Rocket-ChipのBootROMを改造してプログラムを差し替えてみる

Rocket-ChipのBootROMはどのような構成になっているのだろうか? まずはVerilogの実装を見て確認してみよう。emulator/generated-src/freechips.rocketchip.system.DefaultConfig.v を確認してみると、BootROMと言いながら思いっきりハードワイヤードで実装してあった。

  • emulator/generated-src/freechips.rocketchip.system.DefaultConfig.v
module TLROM_bootrom( // @[:freechips.rocketchip.system.DefaultConfig.fir@194725.2]
  input         clock, // @[:freechips.rocketchip.system.DefaultConfig.fir@194726.4]
  input         reset, // @[:freechips.rocketchip.system.DefaultConfig.fir@194727.4]
  output        auto_in_a_ready, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  input         auto_in_a_valid, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  input  [2:0]  auto_in_a_bits_opcode, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  input  [2:0]  auto_in_a_bits_param, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  input  [1:0]  auto_in_a_bits_size, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  input  [8:0]  auto_in_a_bits_source, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  input  [16:0] auto_in_a_bits_address, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  input  [7:0]  auto_in_a_bits_mask, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  input         auto_in_d_ready, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  output        auto_in_d_valid, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  output [1:0]  auto_in_d_bits_size, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  output [8:0]  auto_in_d_bits_source, // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
  output [63:0] auto_in_d_bits_data // @[:freechips.rocketchip.system.DefaultConfig.fir@194728.4]
);
  wire  TLMonitor_clock; // @[Nodes.scala 25:25:freechips.rocketchip.system.DefaultConfig.fir@194735.4]
  wire  TLMonitor_reset; // @[Nodes.scala 25:25:freechips.rocketchip.system.DefaultConfig.fir@194735.4]
  wire  TLMonitor_io_in_a_ready; // @[Nodes.scala 25:25:freechips.rocketchip.system.DefaultConfig.fir@194735.4]
...
    .io_in_d_bits_size(TLMonitor_io_in_d_bits_size),
    .io_in_d_bits_source(TLMonitor_io_in_d_bits_source)
  );
  assign index = auto_in_a_bits_address[11:3]; // @[BootROM.scala 50:34:freechips.rocketchip.system.DefaultConfig.fir@195289.4]
  assign high = auto_in_a_bits_address[15:12]; // @[BootROM.scala 51:68:freechips.rocketchip.system.DefaultConfig.fir@195290.4]
  assign _T_1167 = high != 4'h0; // @[BootROM.scala 52:53:freechips.rocketchip.system.DefaultConfig.fir@195291.4]
  assign _GEN_1 = 9'h1 == index ? 64'h597f1402573 : 64'h1f414130010041b; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]
  assign _GEN_2 = 9'h2 == index ? 64'h840207458593 : _GEN_1; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]
  assign _GEN_3 = 9'h3 == index ? 64'h0 : _GEN_2; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]
  assign _GEN_4 = 9'h4 == index ? 64'h0 : _GEN_3; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]
  assign _GEN_5 = 9'h5 == index ? 64'h0 : _GEN_4; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]
  assign _GEN_6 = 9'h6 == index ? 64'h0 : _GEN_5; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]
  assign _GEN_7 = 9'h7 == index ? 64'h0 : _GEN_6; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]
  assign _GEN_8 = 9'h8 == index ? 64'hbcd5051b0000b537 : _GEN_7; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]
  assign _GEN_9 = 9'h9 == index ? 64'h2345859b000015b7 : _GEN_8; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]
  assign _GEN_10 = 9'ha == index ? 64'h1050007300b50633 : _GEN_9; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]
  assign _GEN_11 = 9'hb == index ? 64'hbff5 : _GEN_10; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]
  assign _GEN_12 = 9'hc == index ? 64'h0 : _GEN_11; // @[BootROM.scala 52:47:freechips.rocketchip.system.DefaultConfig.fir@195292.4]

これは、ブートプログラムをコピーして編集し、ロードする場所をConfig.scalaで変更した。

  • program/simple/simple.S (一部抜粋)
.section .text.hang, "ax", @progbits
.globl _hang
_hang:
        /* 新規追加部分 */
        li      a0, 0x0abcd
        li      a1, 0x01234
        add     a2, a0, a1
1:
  wfi
  j 1b
diff --git a/src/main/scala/coreplex/Configs.scala b/src/main/scala/coreplex/Configs.scala
index ab6d6fa..a6d6068 100644
--- a/src/main/scala/coreplex/Configs.scala
+++ b/src/main/scala/coreplex/Configs.scala
@@ -25,7 +25,7 @@ class BaseCoreplexConfig extends Config ((site, here, up) => {
   case MemoryBusKey => MemoryBusParams(beatBytes = site(XLen)/8, blockBytes = site(CacheBlockBytes))
   // Additional device Parameters
   case ErrorParams => ErrorParams(Seq(AddressSet(0x3000, 0xfff)), maxAtomic=site(XLen)/8, maxTransfer=4096)
-  case BootROMParams => BootROMParams(contentFileName = "./bootrom/bootrom.img")
+  case BootROMParams => BootROMParams(contentFileName = "./program/simple/simple.img")
   case DebugModuleParams => DefaultDebugModuleParams(site(XLen))
 })

github.com

ログファイルを確認すると、正しく実行できているようだ。

cd rocket-chip/emulator
make CONFIG=DefaultConfig output/dhrystone.riscv.out
grep "\[1\] pc" output/dhrystone.riscv.out | less -S

C0:         21 [1] pc=[0000010040] W[r10=000000000000b000][1] R[r 1=62bef4b391e515e7] R[r 0=0000000000000000] inst=[0000b537] lui     a0, 0xb
C0:         22 [1] pc=[0000010044] W[r10=000000000000abcd][1] R[r10=000000000000b000] R[r13=17f201a91c7cccd7] inst=[bcd5051b] addiw   a0, a0, -1075
C0:         23 [1] pc=[0000010048] W[r11=0000000000001000][1] R[r 0=0000000000000000] R[r 0=0000000000000000] inst=[000015b7] lui     a1, 0x1
C0:         24 [1] pc=[000001004c] W[r11=0000000000001234][1] R[r11=0000000000001000] R[r20=17f201a91c7cccd7] inst=[2345859b] addiw   a1, a1, 564
C0:         25 [1] pc=[0000010050] W[r12=000000000000be01][1] R[r10=000000000000abcd] R[r11=0000000000001234] inst=[00b50633] add     a2, a0, a1
C0:         26 [1] pc=[0000010054] W[r 0=ffffffe484e63bc8][0] R[r 0=0000000000000000] R[r 5=17f201a91c7cccd7] inst=[10500073] wfi (args unknown)
C0:         31 [1] pc=[0000010058] W[r 0=000000000001005a][1] R[r31=000000000000abcf] R[r29=17f201a91c7cccd7] inst=[0000bff5] j       pc - 4
C0:         32 [1] pc=[0000010054] W[r 0=ffffffe484e63bc8][0] R[r 0=0000000000000000] R[r 5=17f201a91c7cccd7] inst=[10500073] wfi (args unknown)
C0:         37 [1] pc=[0000010058] W[r 0=000000000001005a][1] R[r31=000000000000abcf] R[r29=17f201a91c7cccd7] inst=[0000bff5] j       pc - 4

計算により、 0x0abcd + 0x01234 = 0x0be01 という計算が正しく行えていることを確認した。

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にも所属しているので、そこからの情報なのかな。 用語集とか、かなりありがたい。

目次

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


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

Pixel Visual CoreもGoogleの開発したモバイル向けのDSAユニットである。 これは、コンピュータビジョン処理に最適化したDSAであり、携帯電話やタブレット上のチップとして、Androidによって制御されるk、音を想定している。

マルチコアのデザインになっており、2から16コアの構成をサポートしており、TPUというよりもSystem on Chip(SOC)として動作することを想定している。

モバイル向けのためTPUよりも構成としては非常に小さく、消費電力も小さく構成されている。

Pixel Visual Coreについて理解するために、まずは用語集を作成した。この用語を参照しながら解説を読み進めていってほしい。

用語 略語 簡単な説明
コア プロセッサ。Pixel Visual Coreは2~16コアを持っている。最初の実装では8コアが使われた。これは「ステンシルプロセッサ(Stencil Processor:STP)」とも呼ばれている。
Halide 画像処理向けのドメイン固有プログラミング言語で。アルゴリズムの形式と実行スケジュールを分離することを目的としている。
Halo 16×16の計算アレイにおいてステンシル計算を実行するにあたり、アレイの境界に近い場所を拡張した領域。この領域は値を持っているが、計算はしない。
画像信号処理プロセッサ ISP 画像の品質を向上させるための固定機能を持ったASIC。カメラを搭載しているすべてのPMDで見つけることができる。
画像処理ユニット IPU GPUと逆の問題を解決するためのDSA。入力画像を解析しコントラストなどを変更することで出力画像を生成する。
Line Buffer Pool LB ラインバッファ処理中の画像を中間結果を格納し、次のステージをすぐに実行することができるようにするためのバッファ。すべてのラインを保持できるように、十分な大きさを用意している。
ネットワークオンチップ NOC Pixel Visual Core内のコアを接続するためのネットワーク
物理ISA pISA ハードウェアで実行される、Pixel Visual Coreの命令セットアーキテクチャ(ISA)
プロセッシングエレメントアレイ 16×16のプロセッシングエレメントとhaloによって処理される16bitの積和演算。各プロセッシングエレメントには、ベクトルレーンとローカルメモリが搭載されている。4方向のどちらにも、データをシフトして転送することができる。
シートジェネレータ SHG 1×1から31×31ピクセルのブロックのメモリアクセスのことを「シート」と呼ぶ。Haloの領域を搭載するかを含めて、他のサイズはオプションである。
スカラーライン SCL ベクトルレーンと演算としては同一なのだが、ジャンプ、分岐、例外、生後命令によりベクトルアレイを制御できる命令が加わっていることが異なる。すべてのシートジェネレータのロードストアをスケジュール管理する。小さな命令メモリも含んでいる。スカラプロセッサ中のベクトルアーキテクチャと同じような役割である。
ベクトルレーン VL プロセッシングエレメントの一部分で、計算を行う。
仮想ISA vISA コンパイラによって生成されるPixel Visual CoreのISA。命令実行前に、物理ISAにマッピングされる。

Pixel Visual Coreは新しいクラスのドメイン固有アーキテクチャで、画像処理のために画像処理ユニット(Image Processing Unit:IPU)というものを搭載している。 IPUはGPUと逆の問題を解決するためのDSAで、入力画像を解析しコントラストなどを変更することで出力画像を生成する。 IPUはDSAなので、CPUやGPUなどが、DSAが苦手とする領域の処理を行ってくれるためすべての処理をDSAで実行する必要はない。 IPUはCNNの項目で開設した、CNN計算に依存している。

Pixel Visual Coreの新しいところは、SIMD命令のような1次元の処理だけでなく、2次元の処理も実行できるようになっている点である。 PEのネットワークは2次元で構成されており、これによりオフチップのメモリにアクセスする必要性も最小化される。 このアーキテクチャにより、CNNアルゴリズムや画像処理の中心であるステンシル計算を、ハードウェアを使ってシンプルに実行することができるようになる。

ハードワイヤードなIPUであるISP

カメラを搭載しているモバイルデバイス(Personal Mobile Device: PMD)には必ず、ハードワイヤードなアクセラレータであるISP(Image Signal Processor)が搭載されている。 ISPは固定機能を持ったASICで、仮想的にすべてのISPに搭載されている。

図7.30は基本的な画像処理システムの構成を示している。 画像処理システムには、レンズ、センサー、ISP、CPU、DRAM、ディスプレイなどが搭載されている。 ISPは画像を受け取ると、レンズとセンサーから人工物を除去し失われた色を補完する。 そして、全体的な画像の品質を向上させる。 PMDは小さなレンズを使うのでピクセルにゴミが乗りやすくなり、したがって高い品質の画像とビデオを生成するためにはこれらの処理は不可欠である。

f:id:msyksphinz:20180106225337p:plain
図7.30 一般的な画像処理プロセッサの構成。画像処理プロセッサ(ISP)、CPU、DRAM、レンズ、センサで構成されている。

ISPプロセッサはハードウェアビルディングブロックをソフトウェアでコンフィグレーションしながら処理を進める。 一般的には、各ステージをパイプライン化してメモリトラフィックを最小化するように設計され、少しのピクセルを入力し、少しのピクセルを出力する。 計算内容は主に隣接したピクセルとのステンシル計算であるが、これらの計算の結果は「ラインバッファ」と呼ばれるバッファに格納される。 ラインバッファにより隣接するピクセルの局所性を使用して、中間画像を保持することによってすべてのラインをキャプチャ氏、中間結果を次のステージに転送することができる。

ISPは効率的であるが、2つの問題点がある。 ポータブルデバイスの処理能力が向上するにつれ、画像の品質は向上していき、ISPのフレキシビリティの無さが問題となってくる。 特に新しいISPをSOC上に設計するためには、数年の歳月を要することになる。 2つ目の問題は、この計算リソースが、画像の品質を向上させるためだけに利用されるということである。 これはPMDにおいて常に必要とされる処理ではない。 現在の世代のISPは高性能向けのPMDでは2Tops/sの処理を実行することができ、DSAに置き換えても同様の処理能力と効率性を達成することができる。

Pixel Visual Coreのソフトウェア

Pixel Visual Coreを一般的なハードワイヤードなパイプライン構成と考えると、ISPと同様に有効非巡回グラフ(directed acyclic graph:DAG)に落とし込むことができる。 Pixel Visual Coreの画像処理プログラムは一般的にHalideで記述される。 Halideはドメイン固有向けの関数型プログラミング言語で、画像処理に適している。 図7.31に画像をぼかす処理をHalideで記述した例を示す。 Halideはプログラムの機能を示す領域と、プログラムのスケジューリングを行う領域を分離して記述することができ、ハードウェア上で機能をどのようにして最適化するかを指定することができる。

Func buildBlur(Func input)
    // 機能記述領域 (ターゲットプロセッサとは独立している)
    Func blur_x("blur_x"), blur_y("blur_y");
    blur_x(x,y) = (input(x-1,y) + input(x,y)*2 + input(x+1,y))/4;
    blur_y(x,y) = (input(x,y-1) + input(x,y)*2 + input(x,y-1))/4;
    
    if (has_ipu) {
        // スケジューリングの領域(ターゲットプロセッサに対してどのように最適化を行えばよいかを示している)
        blur_x.ipu(x,y)
        blur_y.ipu(x,y)
    }
    
    return blur_y;
}
図7.31 画像をぼかす処理をHalideで記述した例

Pixel Visual Coreのアーキテクチャ哲学

Pixel Visual CoreはPMDデバイスで実行されるため、10~20秒で6~8Wを消費し、スクリーンが消されると10mW程度に消費電力が減る。 これらのPMDの消費電力に対応するため、Pixel Visual Coreは相対的なエネルギーコストにおいて調査を行っており、図7.32に示す。 単一の8-bit DRAMアクセスは、12,500個の8-bit加算もしくは7~100回の8-bit SRAMアクセスと同一の消費電力である。 また、IEEE 754の浮動小数点演算のコストは8-bitの整数演算の22倍から150倍の電力を消費し、ダイサイズは大きくなる。

操作 エネルギー(pJ) 操作 エネルギー(pJ) 操作 エネルギー(pJ)
8b DRAM LPDDR3 125.00 8b SRAM 1.2-17.1 16b SRAM 2.4-34.2
32b Fl. Pt. muladd 2.70 8b int muladd 0.12 16b int muladd 0.43
32b Fl. Pt. add 1.50 8b int add 0.01 16b int add 0.02
操作ごとのエネルギーコスト。TSMC 28-nm HPMプロセスでの測定結果。このプロセスはPixel Visual Coreで実際に使用している。

7.2章のガイドラインに加えて、Pixel Visual Coreは以下の観点により設計されている。

  • 2次元は1次元の構成よりも良い
    • 2次元の構成により、画像処理にのデータコミュニケーションの距離を最小化することができる。
  • 遠いよりは、近いほうが良い: データの移動は高価である
    • さらに、データをALUに移動するコストも増加する。DRAMの処理時間とエネルギーコストは、ローカルストレージでのデータ移動に比べるとずっと高い。

IPUの目的は、ハードウェアプログラマビリティを与えることにより画像処理以外の用途にも使うことができるようにすることだ。 Pixel Visual Coreには以下の3つの用途がある。

  1. 2次元構成は1次元の構成よりも優れているという考え方により、Pixel Visual Coreは2次元のSIMDアーキテクチャを採用している。
  2. 2次元アレイ上にプロセッシングエレメント(PE)を配置している。
  3. PEは以下の要素で構成される。
  4. 16-bitというデータ幅は、このドメインで必要なデータ精度を考慮したうえで決定されている。

  5. Pixel Visual Coreは各PEに一時記憶保存が必要である。PEのメモリはコンパイラにより制御されるスクラッチパッドメモリである。PEメモリの論理的なサイズは、16bit×128エントリで、256バイトである。各PEに分離した小さなSRAMを搭載することは実装的に非効率なため、Pixel Visual CoreはPEメモリを8つのPEでグループ化し、1つの幅広いSRAMブロックとして構成している。

  6. PEはSIMDの形式で処理するため、Pixel Visual Coreはすべての読み込みと書き込みの処理をまとめてSRAMにリクエスト薄ことができる。
  7. これはより小さな複数個のSRAMを使うよりも効率的である。
  8. 図7.33に4つのPEの例を示している。

  9. すべてのPEで同時にステンシル計算を実行するため、Pixel Visual Coreは隣接するPEから入力データを集める必要がある。

  10. このために、NSEW(North, South East West)のネットワークを持っている。
  11. PE間でデータを任意の方向にシフトすることができ、このため画像をシフトして行っても、画像が端でロストすることはない。
  12. Pixel Visual Coreはネットワークの一番後ろでトーラス構造をとっている。

ソフトウェアは明示的にデータを所望の方向に移動させることができる。 一方でハードウェアによって制御される死すトリックアレイは、2次元のパイプラインは一方にのみ転送され、ソフトウェアからは見ることができない。

Pixel Visual CoreのHalo

3×3、5×5、7×7のステンシルでは、入力データとして1、2、3個の余分なピクセルを入力する。 Pixel Visual Coreは、

  • 入力値のみを渡すために境界付近でハードウェアを利用する。
  • ALUを除去した単純なPEを使ってわずかに2次元コアを拡張する。

単純なPEと通常のPEは2.2倍の面積の違いがあるため、Pixel Visual Coreは8×8のPEの外に2行2列のPEを拡張している。 この拡張された領域をhaloと呼んでいる。 図7.34は2行2列の拡張を行っており、左上で5×5のステンシル計算を行っている。

f:id:msyksphinz:20180106225320p:plain
図7.34 Pixel Visual Coreの2次元アレイは、中央のPEと周囲のHaloと呼ばれるシンプルなPEコアで構成されている。

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とラインバッファの転送を高速に実行することができる。

Meltdown, Spectre で学ぶ高性能コンピュータアーキテクチャ

巷ではIntel, AMD, ARMを巻き込んだCPUのバグ "Meltdown", "Spectre" が話題です。 これらの問題、内容を読み進めていくと、コンピュータアーキテクチャにおける重要な要素を多く含んでいることが分かって来ました。

つまり、このCPUのセキュリティ問題を読み解いていくと現代のマイクロプロセッサが持つ、性能向上のためのあくなき機能追加の一端が見えてくるのではないかと思い、Google, Intelの文献を読み解いてみることにしました。

が、私はセキュリティの専門家ではありませんし、過去にデスクトップPC向けのような大規模なCPU設計に参加したこともありません。 あくまでコンピュータアーキテクチャに比較的近い場所にいる人間として、この問題の本質はどこにあるのか、可能な限り読み解いていき、現代のマイクロプロセッサが持つ高性能かつ高機能な内部実装について解き明かしていきたいと思います。

と偉そうなことを書きましたが、私自身本質的なところの理解はまだ及んでいません。 3つの問題があると書いてありますが、それが全部根本的な原因は同じに見えるし、概念的なところでしか理解が及んでいないのが悔しい。

参考文献

Intel Analysis of Speculative Execution Side Channels

https://newsroom.intel.com/wp-content/uploads/sites/11/2018/01/Intel-Analysis-of-Speculative-Execution-Side-Channels.pdf

Reading privileged memory with a side-channel

https://googleprojectzero.blogspot.jp/2018/01/reading-privileged-memory-with-side.html

用語集

Googleセキュリティチームのブログには、用語集としてCPUの非常に重要な技術についての解説が簡単に書いてある。 これをきちんと理解するためには、現代のCPUが実装しているアウトオブオーダ実行技術と仮想化・キャッシュメモリのレイテンシについてきちんと理解する必要がある。

  • 投機実行
  • リタイア

An instruction retires when its results, e.g. register writes and memory writes, are committed and made visible to the rest of the system. Instructions can be executed out of order, but must always retire in order. (原文より)

現代のアウトオブオーダプロセッサは過度な投機実行を行うため、その投機実行を行った命令が本当に「正しい」命令であるかどうかを確認する必要がある。

アウトオブオーダ実行により外部メモリアクセスや計算に時間のかかる命令のレイテンシを隠蔽するために、長い命令と無関係な命令は順序を逆転して先に実行される。 特に条件分岐命令などは、条件がTrueかFalseかが分かり、さらに分岐先アドレスが決定されるまでにパイプライン中を何段も通過する必要があり、分岐命令の後続の命令は条件次第で実行すべきであったりそうでなかったりする。

従って、アウトオブオーダプロセッサでは、命令の発行は命令の順序を入れ替えても良いが、リオーダバッファなどの方式を用いて最終的には命令の順番をプログラムどおりに戻す。 分岐命令よりも後続の命令が、分岐命令よりも先に計算を終了したとしても、その結果はプログラムとしては確定に張らない。リオーダバッファに溜め込まれ、前の命令(条件分岐命令)の結果が確定したことをもってその命令が有効かどうかを決定する。このステージで「命令がコミット」されると、その命令が実行されることが確定し、リオーダバッファからレジスタファイルへのデータ書き戻しなどの確定処理が行われる。そうでなければ、その命令はリオーダバッファから「破棄される」。

このとき、コミットされた命令は命令の実行が確定したとして、最後の書き戻し処理を行って「リタイア」される。

リオーダバッファについては私のブログでも言及している。

msyksphinz.hatenablog.com

ちなみに上記の解説記事にも出てくるが、ロード命令は投機実行を行っても良い。しかしストア命令はダメだ。 何故かというと、ロード命令はロード値をレジスタファイルに書き込む前にリオーダバッファなどに格納して「破棄」することができる可能性を残しているが、ストア命令は発行してしまうとメモリの状態を変えてしまうため、破棄することが出来なくなる。従って、ロード命令のみ投機実行が行われ、ストア命令は投機実行されないというのが基本だ。

  • 論理プロセッサコア

A logical processor core is what the operating system sees as a processor core. With hyperthreading enabled, the number of logical cores is a multiple of the number of physical cores. (原文より)

いわゆるマルチスレッディング、ハイパースレッディングの技術である。 物理的なコア数よりも多くのコアがオペレーティングシステムからは見えるようになっており、パイプライン中に複数のプロセスが実行される。

1プロセスではパイプラインを埋めることが厳しいようなプログラムでも、独立した複数のプロセスを同時にパイプライン中に時分割で流すことにより、パイプラインを効率的に活用することが出来る。

  • キャッシュデータ・アンキャッシュデータ

In this blogpost, "uncached" data is data that is only present in main memory, not in any of the cache levels of the CPU. Loading uncached data will typically take over 100 cycles of CPU time. (原文より)

組み込み業界だけの話かと思ったらそうでもない。

一般的にリアルタイムデータや制御データなどのタイムクリティカルなデータは、キャッシュに入れてしまうと実際にコアの外のバスに流れるのがいつになるのか分からないため、アンキャッシュなデータとしてレジスタに持ってきたり、レジスタからストアする。

このようなアンキャッシュなデータはキャッシュを汚すことがないというのが1つの利点であり、またすぐにコア外のバスまで出て行くのでタイミングクリティカルな制御信号などを流すのに使われる。

f:id:msyksphinz:20180105230453p:plain
  • ミスプレディクションウィンドウ

The time window during which the CPU speculatively executes the wrong code and has not yet detected that mis-speculation has occurred.

上記の投機実行では、分岐予測が完了するまでとりあえず次の命令をフェッチして発行し続けるが、実際にはこれらの命令は条件分岐予測が外れることにより破棄されるかもしれない。

ミスプレディクションウィンドウは、この条件分岐命令において分岐予測に失敗することで、どれくらいの後続の命令が発行されるかを示しており、このウィンドウが大きいほど多くの命令が破棄される。

通常のレジスタ比較の命令ではレジスタリードしてから比較するだけのため、このウィンドウは比較的小さいと思われるが、例えばインダイレクトジャンプ命令(レジスタ間接分岐)の場合で、ターゲットのレジスタを外部の遠いメモリからロードしている場合はアドレス確定までかなり時間がかかる。 分岐の成立・不成立だけでなく、正しく分岐アドレスを予測できるのかが重要な鍵となる。

条件分岐予測って一見単純なようだが、

  • 条件を正しく予測できるか
  • 分岐先アドレスを正しく予測できるか

の2つが入り混じっており、結構面倒くさい分野であったりする。

バリアント1: Bounds check bypass

まず最初はこれ。理屈としては結構分かりやすい。

現代のアウトオブオーダプロセッサには、投機実行およびハードウェアプリフェッチという機能がある。 Intelのマニュアルにあるとおり、

Implicit caching occurs when a memory element is made potentially cacheable, although the element may never have been accessed in the normal von Neumann sequence. Implicit caching occurs on the P6 and more recent processor families due to aggressive prefetching, branch prediction, and TLB miss handling.

つまり、キャッシュ可能な領域であれば、ハードウェアが自動的にプリフェッチを走らせることもあれば、投機的にメモリフェッチを行うことがある。 これを、命令順どおりに実行されるわけではないという意味で「von Neumannシーケンス」ではないとしている。 分岐予測のペナルティを軽減するための機能でもあるし、TLBミスが発生する前にハードウェアプリフェッチを行っておくという機能もあるのかもしれない。

つまり、現代の高性能プロセッサはプログラムの意図しないところで勝手にプリフェッチを実行し、なるべくミス時のペナルティを軽減しようとしている。

これが間接分岐予測だとさらに問題は面倒になる。 レイテンシが長いため、もしL1→L2→L3と全てミスして外部までデータを取りに行き、順番にキャッシュを汚していく可能性があり、意図しないデータをキャッシュに持ってきている可能性がある。

以下の例が分かりやすい。

struct array {
 unsigned long length;
 unsigned char data[];
};
struct array *arr1 = ...; /* small array */
struct array *arr2 = ...; /* array of size 0x400 */
/* >0x400 (OUT OF BOUNDS!) */
unsigned long untrusted_offset_from_caller = ...;
if (untrusted_offset_from_caller < arr1->length) {
 unsigned char value = arr1->data[untrusted_offset_from_caller];
 unsigned long index2 = ((value&1)*0x100)+0x200;
 if (index2 < arr2->length) {
   unsigned char value2 = arr2->data[index2];
 }
}

条件分岐(if文)にarr1->lengthの要素が使われているが、この要素は実際に値を取ってくるまで分からない。

しかしプロセッサはif文が成立すると予測した場合、勝手にarr1->data[untrusted_offset_from_caller]をハードウェアがフェッチを行い、そのデータをキャッシュまで持ってきている可能性があるということだ。

バリアント2: Branch target injection

これも分岐予測についての基本的な考え方を理解するための良い教材だ。 ってか自分も良く分かっていないことが多い。

KVM(Kernel VM)という機構については少しだけ聞いたことがあるのだが、しっかり調べてみると以下のウェブサイトに行き着いた。 http://www.atmarkit.co.jp/ait/articles/0903/12/news120.html なるほど、要するにハードウェアの仮想化支援機能を使った仮想化であり、ベースにはLinuxなどのOSが存在する。 VMwareVirtualBoxと違い、ホストOSの上に仮想化層が必要ない、これによりオーバヘッドを削減することができる。

Haswellの分岐予測機構の構造

ここから、Haswellの分岐予測機構について怒涛の解説が始まる。

分岐予測といっても、その方式は1つではなく、命令の種類によって多くの分岐予測機構が存在し、その特性によって使い分ける。

通常の分岐予測機構

PC相対、レジスタ値やフラグなどの値に応じて分岐が成立するかを決める。この場合は分岐先アドレスは固定(PC相対)であることが多い。

MIPSRISC-V命令では、BEQ, BNEなどの命令がこれに該当し、レジスタ値を比較してPC相対でジャンプする。

間接ジャンプ(間接呼び出し)

関数呼び出しや関数ポインタをイメージすればよい。関数の存在場所はメモリ中にアドレスリストとして格納されており、CPUはそのアドレス情報をロード命令を使ってレジスタに取得し、レジスタ相対でジャンプする。

従って、この場合はレジスタ相対ジャンプであり、レジスタの値が決定するまでジャンプ先アドレスが決まらない。また、MIPSRISC-V等でいうjal命令のように、条件分岐というよりも分岐することは決まっており、その分岐先アドレスを予測することが重要になる。

関数戻りアドレスを予測する

これは間接ジャンプと考え方は良く似ているが、アドレスが予測しやすいタイプである。 関数呼び出しは、よっぽどのことがない限り関数が終了すると呼び出し元に戻ってくる。 従って、関数呼び出しの命令(例えばjal命令)が実行されれば、関数から戻ってくるときは必ずjal命令の次のアドレスからフェッチが始まるはずだということが分かる。

従って、この関数呼び出しの場所をスタックに保存しておき、関数から戻るとき(ret命令などが実行された場合)は、次の関数フェッチ先をスタックから取り出して命令フェッチに使用する。

一般的な分岐予測機構の仕組み

分岐予測を実現するためにはいくつかの機構が必要だが、ここでは、分岐ターゲットバッファ(Branch Target Buffer:BTB)、分岐履歴テーブル(Branch History Table:BHT)が紹介されている。

分岐ターゲットバッファはその名のとおり、ソースアドレス(現在のPCアドレス)から、次にどの命令をフェッチするか(分岐先はどこか)を記憶しておくバッファである。 Haswellの分岐ターゲットバッファはソースアドレスとしてPCの下位32ビットを使うとしている。例えばPCアドレス 0x4141_0004_1000 で分岐が実行され 0x4141_0004_5123 にジャンプしたとすると、そのジャンプ履歴がBTBに記録される。

PCアドレスのうち32ビットよりも上位のアドレスはBTBの検索には利用しない。とりあえず下位の32ビットを使ってソースアドレスの参照を行い、さらに後続のステージで分岐タグバッファを参照して32ビットよりも上のビットをチェックし、上位ビットもマッチすれば分岐予測がヒットしたとしてBTBのエントリ値が次のフェッチアドレスとして使用される。

しかし、下位32ビットを使うといっても、32ビットのアドレスエントリを持つBTBバッファを作ってしまうと、232のエントリを持つBTBテーブルを作る必要が生じてしまい、ここはエントリアドレスをさらに省略してBTBのエントリ数を減らす。

ここでアドレス圧縮(というかXORによるビット数減らし)が行われる。

bit A bit B
0x40.0000 0x2000
0x80.0000 0x4000
0x100.0000 0x8000
0x200.0000 0x1.0000
0x400.0000 0x2.0000
0x800.0000 0x4.0000
0x2000.0000 0x10.0000
0x4000.0000 0x20.0000

またこの表が非常に分かりにくい。 要するにこの表を使って、アドレスのマスクを行い、bitAとbitBの1になっている部分のアドレスビットを互いにXORし、その結果をBTBのエントリアドレスの一部として利用する。

f:id:msyksphinz:20180105234119p:plain
f:id:msyksphinz:20180105234316p:plain
  • 例その1. アドレス 0x0100_0000 と 0x0180_0000 は、23ビット目と14ビット目のXORが、0 xor 0 = 0, 1 xor 0 = 1 なので互いに異なり識別可能。
  • 例その2. アドレス 0x0100_0000 と 0x0180_8000 は、24ビット目と15ビット目のXORが、0 xor 0 = 0, 0 xor 1=1だが、23ビット目と14ビット目のXORが、0 xor 0 = 0, 1 xor 0 = 1 , なので互いに異なり識別可能。
  • 例その3. アドレス 0x0100_0000 と 0x0140_2000 は、26ビット目と17ビット目のXORが、0 xor 0 = 0, 1 xor 1=0なので識別できない。
  • 例その4. アドレス 0x0100_0000 と 0x0180_4000 は、27ビット目と18ビット目のXORが、0 xor 0 = 0, 1 xor 1=0なので識別できない。

となってしまい、最後の2つのアドレスはBTBエントリにとって区別がつかなくなる。

間接分岐ジャンプにも利用される分岐履歴テーブル

上記の例では、非常に単純なBTBのアドレスエントリ生成方式を見たが、実際には分岐履歴テーブル(Branch History Table:BHT)、もしくは分岐履歴バッファ(Branch History Buffer:BHB)との組み合わせで利用される。

Haswellには、この分岐履歴テーブルが29本あるとしている。 分岐履歴テーブルを利用して過去の29回分の分岐履歴(分岐した場合のみ)を格納している。

29本あるといっても何だか分からないので、もう少し噛み砕いて分岐履歴テーブルを解説してみよう。

局所分岐予測と広域分岐予測の考え方の違い

分岐予測には大きく分けて局所分岐予測と広域分岐予測の2種類の考え方がある。

局所分岐予測というのは、大学の学部の授業でも頻繁に登場する考え方だ。

「このアドレスの分岐命令は過去何度も分岐が成立しているから、次も成立するだろう」という考え方である。つまり、現在のPCアドレスとそのアドレスの分岐命令の結果しか見ないので、局所分岐予測と言われる。

ところが、プログラムのシーケンス的にはそうでない場合も存在する。

「このアドレスの分岐命令を実行する前に、1つ前はAの場所で分岐が成立、2つ前はBの場所で分岐外不成立だった。このパターンを見ると次の分岐は成立と予測できる」なんてケースがある。 これはC言語などで、if文を並べて書いているとこのような状況が普通に起きる。 つまり、今現在のPCアドレスの分岐命令の結果だけでなく、その1つ前に実行された別のPCアドレスの分岐予測の結果、さらに、過去の別PCアドレスの分岐予測の結果まで考慮して分岐予測を実行する。 このためこれを大域分岐予測と呼ぶ。

実際には、局所分岐予測の場合には各分岐命令毎に分岐履歴バッファを持ち、広域分岐予測の場合は、全ての分岐命令で共通の分岐履歴バッファを使うため面積は削減できる。

その代わり、広域分岐予測の場合は分岐履歴バッファを長めに保持する必要があり、履歴バッファが長くなるとそのパタンをアドレスとして用いるパタン履歴テーブルのエントリ数も長くなってしまうという弱点がある。 そして広域分岐予測は、十分テーブルを大きくしても局所分岐予測よりも成績が少し悪い。

このBHBのアップデートを示した擬似コードが、以下だ。分岐を実行したソースアドレスとそのとび先アドレスを次々とXORしていることが分かる。これにより、分岐のシーケンスが記憶され、次の分岐予測に使用される。

void bhb_update(uint58_t *bhb_state, unsigned long src, unsigned long dst) {
 *bhb_state <<= 2;
 *bhb_state ^= (dst & 0x3f);
 *bhb_state ^= (src & 0xc0) >> 6;
 *bhb_state ^= (src & 0xc00) >> (10 - 2);
 *bhb_state ^= (src & 0xc000) >> (14 - 4);
 *bhb_state ^= (src & 0x30) << (6 - 4);
 *bhb_state ^= (src & 0x300) << (8 - 8);
 *bhb_state ^= (src & 0x3000) >> (12 - 10);
 *bhb_state ^= (src & 0x30000) >> (16 - 12);
 *bhb_state ^= (src & 0xc0000) >> (18 - 14);
}
f:id:msyksphinz:20180105235801p:plain

分岐予測器の内部をリバースエンジニアリングする

Googleのチームでは以下のようなテストを行っている。 2つのプログラムを、1つの物理コア(ただし2つのプロセス:論理コア)で実行する。

ここで、ASLRというのはアドレスをランダム化する機構で、これを使ってしまうと2つのプログラムで使用するPCアドレスがずれてしまうためこの機能を無効化している。

ここで再現したいのは、2つのプログラムで分岐予測テーブルが共有されてしまいことにより他のプロセスの情報が見えてしまうことである。

そのために、関数ポインタのコールを行いテスト変数へのアクセスを実行する。これによりテスト変数はキャッシュに格納される。 その後その値をCLFLUSH(これはx86のキャッシュラインのフラッシュ命令である)を使ってフラッシュしてしまう。 次に、「現在の分岐予測器の状態」を作り出すために、必ず成立する分岐命令をN回実行する(ここで「必ず成立する」というのは、上述したとおりBHBは成立した分岐しか記録しないためだ)。

そしてそれぞれインスタンス1とインスタンス2が値をテストしているときに、再びテスト値をキャッシュに読み出す。このとき、インスタンス1とインスタンス2が同じ分岐ヒストリのパタンを使えば、インスタンス2も分岐予測のレイテンシを埋めるために投機的にテスト値を自分のレジスタに持ってくるだろう。

その後インスタンス1とインスタンス2でテスト値を取得したところ、インスタンス2はインスタンス1の値を見ることが出来たというわけか。

ここでNの数を増やしていくことを考える。上述したようにBHBはプログラムのソースアドレスとターゲットアドレスの情報をXORで記録しているはずだ。 N=25とすると、この間違いは発生しなかった。つまり、N=25までは分岐予測器は2つのインスタンスの違いが識別できている。 ところが、N=26とすると途端にこの現象が発生し始めた。 従って、Haswellの場合はこの少なくとも26個までの分岐履歴を保存しているということになる。

バリアント3: Rogue data cache load

バリアント3の解説について指摘があり、少なくとも過去の解説について重大な誤りがありました。申し訳ありません。文献をよく読み直し修正しましたが、この解説も疑問が残り誤っている可能性があります。鵜呑みにしないようよろしくお願いします。

こちらも投機的実行を使ったもので、ユーザモードのプログラムが、カーネルモードの領域を参照するプログラムの投機実行を行った途中結果までを利用してデータを読み取るという巧妙なものになっている。

まずは以下の解説を読むべし。

投機的実行のハードウェア構造

こちらは原文から参照させてもらっている、アウトオブオーダプロセッサの構成だ。

これを見ると、パイプラインを流れていく命令は、リオーダバッファを通過した後に命令の種類ごとに異なるユニットに発行されることが分かる。 ポートは0から8まで用意されており、整数命令はポート0,1、ロードストアは2,3といった具合だ。

そして重要な点は、違うポートに発行された命令は、プログラムの順番とは独立に実行が進んでいき、ポーとさえ違っていれば、レイテンシの短い命令はレイテンシの長い命令を追い越すことができる。 これが非常に重要な役割を持つ。

もう一つの特徴は、ある命令で例外が発生したとしても、実際に例外が発生するのは命令が実行ユニットを通過し、命令がリタイアするときにのみ例外が発生するということだ。

例えば極端な例を挙げてみる。

load  dst1, mem[A]      // dst2 <- mem[A] のメモリロード
add.f dst2, dst2, 10.0  // dst2 <- dst2 + 10.0 の浮動小数点演算

ここで、2番目のadd.fで浮動小数点例外が発生したものとしよう。 ところがレイテンシ的にloadのほうが時間がかかり、add.fの方が先に命令の実行が終了し、例外を検出できたものとする。

しかし1番目のloadの完了を待たずに、add.fの例外に飛んでしまうと、プログラムの意味的におかしなことになってしまう。 ソフトウェア的にみると、loadが完了していないのに勝手にadd.fの例外が発生したとして誤動作としてとらえられてしまい、これを防ぐために、必ず例外はリオーダバッファによって命令が完了するときに実行される。

リオーダバッファが命令を完了させるときは、必ず命令はプログラムの順番通りに戻っているため、add.fの例外発生(つまりadd.fが完了しリタイアする)ときは、それよりも前に発行されたloadも必ず終了しており、プログラム的に意味を損なわない、という訳だ。

f:id:msyksphinz:20180106190624p:plain

さて、以上の前提に基づいて、最初の原文に戻って問題を解析してみる。

アウトオブオーダによってカーネルモード命令を結果をうまく使いまわす仕組み

まず、以下のプログラムを考えてみる。

mov rax,[somekernelmodeaddress]

これをユーザモードで実行した場合、余裕で例外が発生する。ダメに決まってる!

しかし、例外が発生するのは、「リオーダバッファに入って命令が完了するとき」であるということを思い出してほしい。 つまり、実際にカーネルモードにアクセスを行い、データをリオーダバッファ中、もしくはキャッシュに持ってきている可能性がある。 これを原文中では、「マイクロアーキテクチャの状態を変更している可能性」としている。

次のプログラムだ。

mov rax, [Somekerneladdress]
mov rbx, [someusermodeaddress]

上記のアウトオブオーダ実行の図を見ると、ロードストアの命令ユニットは2つあるようだ。 しかも上記の2命令には依存関係がないので、この2つは同時に実行することができる。 そして1番目の命令は例外を発生しているのだが、2番目の命令はすでにロードを行っているか、少なくともキャッシュに[someusermodeaddress]のデータをロードしているかもしれない。 これは例外が発生した後に、当該キャッシュをアクセスしてみると確認することができる。

次のプログラムだ。これは上記と違って依存関係を持っている。

mov rax, [somekerneladdress]
and rax, 1
mov rbx,[rax+Someusermodeaddress]

ここで、2番目と3番目の命令が同時に発行されているものとする。

重要なのは2番目のmovの実行結果は、raxつまりsomekerneladdressの値に依存しているということだ。 つまり、アウトオブオーダで2番目のmovの途中結果がキャッシュに保存されていると、その情報を頼りにsomekerneladdressの値を特定することが可能になる!

ここで原文の著者が随分と苦労しているのが、じゃあ1番目の命令が例外を発生させる前に、どうにかして2番目と3番目の命令を実行させなければならない。

これを行うために、mov命令で使用するLoad/Storeとは別のユニットを使って実行するand命令を挿入してある。 Load/Storeユニットを使う命令とは別の命令挿入することで、CPUはこれらの命令を投機的に実行することができるようになる。 (筆者の解釈: ここの説明は正直よく分からない。別のユニットを使っても、投機的な実行を確認することにはならないのでは?)

正しく理解できているか微妙だが、おそらくこういうことだ。

最初のmov命令がカーネル領域のデータをraxにロードするが、実際にはこの命令は例外を発生させる。 この例外が発生する前に、投機的実行によりand命令と次のmov命令を実行することができれば、その痕跡をキャッシュに残すことができるだろう、という考え方だろう。

f:id:msyksphinz:20180106190644p:plain

ただし、この考え方も合っているかは疑問が残る。 そもそも、最初のmovがリタイアするまでに十分な時間が確保したい場合、最初のmovの前にさらに時間がかかる処理を挿入すれば、リタイアまでの時間を大幅に稼ぐことができるのではないか? その間に2番目と3番目の命令を投機実行させることで、ユーザモードのデータ領域をL1に十分な余裕をもって残すことができそうなのだが...

// この部分に非常にレイテンシの長い命令を挿入する。
mov rax, [somekerneladdress]       // ↑の命令の影響でこの命令はなかなか例外を出すことができない
and rax, 1                         // その間に、この命令と
mov rbx,[rax+Someusermodeaddress]  //           この命令がkerneladdressのデータに基づいてユーザ領域のデータを取り出す。

まあ、分からん。

マイクロアーキテクチャから見たこれらの脆弱性の修正方法

また、Intelの公式声明でもこれらの解決法(Mitigation:緩和法?)について言及されている。

Bound check Bypass の解決方法

基本的にはソフトウェアによる解決法が望まれている様子。 実際にアクセスを行ってはいけないシーケンスについては、FENSE命令を使って明示的にメモリアクセスの同期を行い、ハードウェアプリフェッチを防ぐという方式。

Branch Target Injection の解決方法

こちらはあまりパッとしないのだが、プロセッサとシステムソフトウェアの間に新しいインタフェースを仕込むという風になっている。 マイクロコードによるアップデートとしては、

  • Indirect Branch Restricted Speculation (IBRS): 間接分岐の投機実行を制限する。
  • Single Thread Indirect Branch Predictors(STIBP): 間接分岐の投機的移行は、1つのスレッドのみが実行できるように制約する。
  • Indirect Branch Predictor Barrier(IBPB): 前のプログラムの挙動が、間接分岐の分岐予測に影響しないようにする。

Rogue Data Cache Load の解決方法

これはユーザモードとスーパバイザモードのページ構造を分け、分離することで解決できるとしている。

まあどっちにしても、ソフトウェアによる修正が必要ということだな。

Computer Architecture 6th Editionの7章"Domain-Specific Architecture" を読む (7.6章 Intel Crest, 学習のためのデータセンタアクセラレータ)

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.6章は DSAの一つの例としてIntel Lake Crestを取り上げている。こちらはかなり情報が限られている。ってか2017年中にリリースって言ってるのに2018年になってしまいましたね。

目次

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


7.6 Intel Crest, 学習のためのデータセンタアクセラレータ

7.3章の最初の引用にあるように、IntelはDNNのためのDSAを製造することをプレスリリースした。 最初の例は、私たちがが本書を執筆している最中にアナウンスされたCrestである。 まだ情報が少ないながら、私たちがこのプロジェクトについて言及しようと思ったのは、Intelというマイクロプロセッサの伝統的な供給メーカがDSAとタッグを組むという重要なステップに踏み出したからである。

CrestはDNNの学習フェーズをターゲットとしている。 IntelのCEOは、3年で機械学習の学習フェーズの速度を100倍にすることが目標であるといった。 図7.6によると、DNNの学習には1か月程度必要である。 このDNNの学習時間を、CEOが言及した通り、1/100であるたった8時間に短縮するということ自体には需要がある。 間違いなく、次の3年でより複雑になり、学習にはより多くの時間が必要となるだろう。 したがって、学習時間を100倍高速化するということについて、過度の危険性はない。

Crestの命令は32×32の行列のブロックを操作する。 Crestは「flex point」と呼ばれる数値形式を用い、これは固定小数点をスケーリングしたものである: 16bitデータを持つ32×32の行列は命令セットの一部から供給される5ビットの指数部を共有する。

図7.28はLake Crestチップのブロックダイアグラムを示している。 これらの行列を計算するために、図7.28に示すようにCrestは12個のプロセッシングクラスタを用意している。 各クラスタには大容量のSRAM、代数用のプロセッシングユニット、オンチップ・オフチップ用ルーティング用の少量の論理が搭載されている。 4つの8GBのHBM2 DRAMモジュールが搭載されており、1TB/sのメモリバンド幅を供給しており、これによりCrestチップは魅力的なRooflineモデルを提供している。 Lake Crestチップは高いバンド幅のインターコネクトをサポートしており、プロセッシングクラスタ内のコアの計算や、共有メモリを介在しないコア間の通信もサポートしている。 Lake Crestの目標はGPUと比較して学習速度を10倍高速化することである。

図7.28には、12個のチップ間リンク(Inter Chip Link:ICL)と、2個のチップ間コントローラ(Inter Chip Controller)を搭載しており、Crestは他のCrestチップと強調して動作することができる。 これは48個のFPGAが専用ネットワークを通じて強調動作するMicrosoftのCatapultと考え方が似ている。 学習速度の100倍の高速化には、いくつかのCrestチップによる強調動作が必要とされる。

図7.28 Intel Lake Crestsプロセッサのブロックダイアグラム。Intelの資料より抜粋。CrestはTSMC28 nmプロセスの古レチクルで実装され、ダイサイズは600から700m2になる予定。このチップは2017年に供給される予定である。IntelはKnights Crestと呼ばれる、Xeon x86コアとCrestアクセラレータのハイブリッドチップも設計している。