FPGA開発日記

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

Spectre & Meltdown を防ぐマイクロアーキテクチャSafeSpecの論文を読む

Hisa Ando氏のブログで知ったのだが、Spectre & Meltdownを防ぐマイクロアーキテクチャとしてSafeSpecという技術が発表されたので、これを読んでみることにした。

https://arxiv.org/pdf/1806.05179.pdf

Hisa Ando氏の記事にも書いてある通り、SafeSpecでは投機的実行の副作用を削減するために、Shadowキャッシュを設けている。 今回はL1Dキャッシュ、L1Iキャッシュ、TLBキャッシュに対してShadowキャッシュを適用して、投機的実行が最終的に完了するまで、完了前の投機実行の命令結果を格納しておき、投機実行が完了すると命令結果を実キャッシュに書き込む。

下記の図において、各キャッシュの横にShadowキャッシュを置き、実キャッシュが投機実行により汚れてしまうことを防いでる。

f:id:msyksphinz:20180619234056p:plain

ここでおそらく誰もが想像するのが、このShadowキャッシュが汚れてしまったことによる副作用によりSpectre攻撃が起こされるのではないか?ということ。 その問題について言及しているのが、おそらく本文中に入っている"Transient Speculative Attack"という問題だと思う(あってるかな?)。

(これ、日本語の資料をあさっても、「さらに発見された脆弱性」としているけど、そりゃキャッシュをさらに追加したんだから発生するのは当然だろう... 別に彼らが新しく発見した脆弱性でも何でもない)。

この追加されたShadow キャッシュ、問題はどれくらいの大きさであるかというところである。 もしこれが小さいと、ShadowキャッシュがFullになったことにより発生するストールなどを観測すれば再びSpectreのような攻撃化可能になることが考えられる。十分に大きくすることが求められるが、そうすると面積としてオーバヘッドがある。

f:id:msyksphinz:20180619233222p:plain
図. 命令キャッシュ・データキャッシュのアクセスの99.99%をフィットさせるために必要なエントリ数
f:id:msyksphinz:20180619233631p:plain
図. iTLB / dTLBのアクセスの99.99%をフィットさせるために必要なエントリ数

彼らの論文によると、命令キャッシュについては25ライン、i-TLBについては10ライン以下、d-TLBについては25ライン以下となっている。さらにグラフを見るとDキャッシュについては最大で60エントリ超が必要だとしている。 結構な面積が必要だと思うがどうだろう...

ついでに言うと、これはベンチマークプログラムを使ったことによる解析なので参考にはならないと思う。

攻撃プログラムというのはベンチマークプログラムのような特性ではないのだし、特殊な特性を持つプログラムに対してもこの防御手段が有効かどうかを考える必要がある。 これは論文をざっくり読んだ感じだとNoではないのだろうか。

検証方法については、サイクルベースのシミュレータを改造することにより実現している。 MARSSx86と呼ぶ。これは初めて知った。

marss86.org

こういうのすごく面白いと思う。マイクロアーキテクチャの研究にはこういうシミュレータを使いこなすことが重要だなあ。

いずれにしろ、ざっくりと呼んだだけなので、もう少し深く読み進めていきたい。

TensorFlow+Kerasに入門(4. Keras2のConvolution2DとConv2Dの違い?)

f:id:msyksphinz:20180701195704p:plain

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

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

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

前回のcifar10のサンプルコードはKeras2のコードで、"Conv2D"と"Convolutional2D"の記述が異なる。 あまりメンテナンスのされていないkeras2cppは"Convolutional2D"しか読み取れないらしく、仕方がないので以下のようにkeras2cppのコードを修正して動作させてみたのだが、自宅の環境ではメモリ不足で途中終了。 急遽メモリの多いマシンを用意して実行してみたのだが、それでもなぜか終了せずに途中で止まってしまった。

  • keras2cpp/keras_model.cc
--- a/machine_learning/tensorflow/keras/keras_model/keras_model.cc
+++ b/machine_learning/tensorflow/keras/keras_model/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") {
diff --git a/machine_learning/tensorflow/keras/machine-learning b/machine_learning/tensorflow/keras/machine-learning

これはやはり適当にConvolutional2DとConv2Dを変換したからなのかな?よく分からないので、万全を期すために全部Convolutional2Dに変換して実行してみたい。

そこで、Keras2のサンプルコードを以下のように書き換えて再度学習させた。これでまた再学習だ。3時間程度かかる...

  • keras/example/cifar10_cnn.py
diff --git a/examples/cifar10_cnn.py b/examples/cifar10_cnn.py
index 1daed4ab..dbc28d3d 100644
--- a/examples/cifar10_cnn.py
+++ b/examples/cifar10_cnn.py
@@ -10,7 +10,8 @@ from keras.datasets import cifar10
 from keras.preprocessing.image import ImageDataGenerator
 from keras.models import Sequential
 from keras.layers import Dense, Dropout, Activation, Flatten
-from keras.layers import Conv2D, MaxPooling2D
+# from keras.layers import Conv2D, MaxPooling2D
+from keras.layers import Convolution2D, MaxPooling2D
 import os

 batch_size = 32
@@ -32,17 +33,17 @@ y_train = keras.utils.to_categorical(y_train, num_classes)
 y_test = keras.utils.to_categorical(y_test, num_classes)

 model = Sequential()
-model.add(Conv2D(32, (3, 3), padding='same',
+model.add(Convolution2D(32, (3, 3), padding='same',
                  input_shape=x_train.shape[1:]))
 model.add(Activation('relu'))
-model.add(Conv2D(32, (3, 3)))
+model.add(Convolution2D(32, (3, 3)))
 model.add(Activation('relu'))
 model.add(MaxPooling2D(pool_size=(2, 2)))
 model.add(Dropout(0.25))

-model.add(Conv2D(64, (3, 3), padding='same'))
+model.add(Convolution2D(64, (3, 3), padding='same'))
 model.add(Activation('relu'))
-model.add(Conv2D(64, (3, 3)))
+model.add(Convolution2D(64, (3, 3)))
 model.add(Activation('relu'))
 model.add(MaxPooling2D(pool_size=(2, 2)))
 model.add(Dropout(0.25))

結果のモデルを保存して、モデル情報と重みファイルを抽出したのだが、やはりConv2Dが使用されているぞ? Keras2をつかったからかしら。Keras2のConvolution2DとConv2Dって根本的にいっしょなのかなあ。

ちなみにコンパイル後の動作も変わらず、最後まで実行できなかった。 うーん、Kerasの中身の実装を見てみる必要性があるか...

AWS上で動作するRISC-VチップFireSimのチュートリアルを試す 2. インスタンスの設定とAWSの起動

f:id:msyksphinz:20180617195844p:plain

AWSで動作するRISC-Vシミュレーション環境FireSimのチュートリアルその2.

[高度な詳細]を開き、テキストボックスに付録に示すようなテキストファイルを入力する。

f:id:msyksphinz:20180617184019p:plain

...

f:id:msyksphinz:20180617184457p:plain

次にストレージを追加する。デフォルトでは75GBと設定されているが、これを300GBまで拡張する。 Vivadoでの合成を含めると、とても75GBでは足りないからだ。

f:id:msyksphinz:20180617185048p:plain

設定が完了すると、インスタンスを起動する。

インスタンスの起動

FireSimのマニュアルにはbashではなくmoshを使うことを推奨している。使ったことがないけど、必要ならば移行することも考えよう。

ログインして、起動状態を確認する。ログインにはAWSで設定したpemファイルを指定する必要がある。これはAWS 上記で設定した起動スクリプトが正しく動作しているかを確認するためだ。

centos@ip-192-168-4-15.ec2.internal:~$ cat machine-launchstatus
machine launch script started
machine launch script completed

machine launch script completed と表示されていれば、起動処理が完了していることを示している。

次にfiresimのリポジトリをcloneする前に、AWSにログインするのに使用したpemファイルをホームディレクトリに配置する。

Now that our manager instance is started, copy the private key that you downloaded from AWS earlier (firesim.pem) to ~/firesim.pem on your manager instance. This step is required to give the manager access to the instances it launches for you. また、パーミッションを変えておく。

$ chown 600 ~/firesim.pem

FireSimリポジトリの起動

FireSimリポジトリをダウンロードして、初期化スクリプトを立ち上げる。

$ git clone https://github.com/firesim/firesim
$ cd firesim
$ ./build-setup.sh fast

つぎに、環境変数などを設定する。下記の操作は、FireSimを動作させるマシンにログインするときは毎回必ず実行しなければならない。

$ source sourceme-f1-manager.sh
success: firesim.pem available in ssh-agent

FireSimの設定を完了する。

$ firesim managerinit
FireSim Manager. Docs: http://docs.fires.im
Running: managerinit

Running aws configure. You must specify your AWS account info here to use the FireSim Manager.
[localhost] local: aws configure
AWS Access Key ID [None]: 【AWSアクセスキーを入力する】
AWS Secret Access Key [None]: 【AWSシークレットアクセスキーを入力する】
Default region name [None]: us-east-1
Default output format [None]: json
Backing up initial config files, if they exist.
Creating initial config files from examples.
If you are a new user, supply your email address [abc@xyz.abc] for email notifications (leave blank if you do not want email notifications): 【Notification用のメールアドレスを入力する】
You should receive a message at
masayuki.kimura.1986@gmail.com
asking to confirm your subscription to FireSim SNS Notifications. You will not
receive any notifications until you click the confirmation link.
FireSim Manager setup completed.
The full log of this run is:
/home/centos/firesim/deploy/logs/2018-06-17--10-55-14-managerinit-8OCEHY65NGAE34J7.log

付録: [高度な詳細]でテキストボックスに追加するコード

#!/bin/bash
echo "machine launch script started" > /home/centos/machine-launchstatus
sudo yum install -y mosh
sudo yum groupinstall -y "Development tools"
sudo yum install -y gmp-devel mpfr-devel libmpc-devel zlib-devel vim git java java-devel
curl https://bintray.com/sbt/rpm/rpm | sudo tee /etc/yum.repos.d/bintray-sbt-rpm.repo
sudo yum install -y sbt texinfo gengetopt
sudo yum install -y expat-devel libusb1-devel ncurses-devel cmake "perl(ExtUtils::MakeMaker)"
# deps for poky
sudo yum install -y python34 patch diffstat texi2html texinfo subversion chrpath git wget
# install DTC. it's not available in repos in FPGA AMI
DTCversion=dtc-1.4.4
wget https://git.kernel.org/pub/scm/utils/dtc/dtc.git/snapshot/$DTCversion.tar.gz
tar -xvf $DTCversion.tar.gz
cd $DTCversion
make -j16
make install
cd ..
rm -rf $DTCversion.tar.gz
rm -rf $DTCversion

# get a proper version of git
sudo yum -y remove git
sudo yum -y install epel-release
sudo yum -y install https://centos7.iuscommunity.org/ius-release.rpm
sudo yum -y install git2u

# bash completion for manager
sudo yum -y install bash-completion

# graphviz for manager
sudo yum -y install graphviz python-devel

# these need to match what's in deploy/requirements.txt
sudo pip install fabric==1.14.0
sudo pip install boto3==1.6.2
sudo pip install colorama==0.3.7
sudo pip install argcomplete==1.9.3
sudo pip install graphviz==0.8.3
# for some of our workload plotting scripts
sudo pip install matplotlib==2.2.2
sudo pip install pandas==0.22.0

sudo activate-global-python-argcomplete

# get a regular prompt
echo "PS1='\u@\H:\w\\$ '" >> /home/centos/.bashrc
echo "machine launch script completed" >> /home/centos/machine-launchstatus

AWS上で動作するRISC-VチップFireSimのチュートリアルを試す 1. 立ち上げと参考文献

参考にしたのは以下。

Welcome to FireSim’s documentation! — FireSim documentation

  1. 初期セットアップ・インストール
    1. 最初にAWSのユーザグループを作成する。AWSのアカウントと支払を行っていればこのステップを実行する必要はない。
    2. アカウントに必要なAWSのリソースを設定する。これによりVPC/サブネット/セキュリティグループなどの、FireSimを実行するために必要な構成を立ち上げる。
  2. 単一ノードのシミュレーションチュートリアル: このチュートリアルでは、1コアのシミュレーションを行い、1つのf1.2xlargeであらかじめビルドされたFireSim AGFIを動作させる。
  3. クラスタシミュレーションのチュートリアル : 個のチュートリアルでは、8ノードのクラスたちシミュレーションを行い、1つのf1.16xlarge上であらかじめビルドされたFireSim AGFIとスイッチモデルを立ち上げる。
  4. オリジナルのハードウェアデザインを立ち上げる(ChiselからFPGAイメージへの変換) : このチュートリアルでは、Rocket ChipのRTLと、Rocket Chipに接続する任意のカスタムRTLを使ってFireSim AGFIを作成してシミュレーションする。ChiselのElaborationと、FAME-1への変更、VivadoのFPGAフローを自動的に実行する。

AWSアカウントのセットアップ

West Virginiaリージョンにて、AWS F1インスタンスを使用するためのセットアップを行う。

2.2. Configuring Required Infrastructure in Your AWS Account — FireSim documentation

キーペアはもともと持っているので、それを使用することにした。F1インスタンスの上限の解除についてもすでに行っているので良しとする。。

t2.nanoインスタンスを作成する

ローカルマシンでパッケージのインストールをするのは面倒なため、t2.nanoインスタンスを立ち上げてAWSを構成するためのコマンドを実行する。 t2.nanoインスタンスAWSの構成を行うためだけに使用し、その後は破棄する。

t2.nanoインスタンスは、Amazon Linux AMIをインストールして立ち上げる。起動してログインする。

f:id:msyksphinz:20180617142219p:plain

AWSのセットアップを行う。

[ec2-user@ip-172-31-17-43 ~]$ aws configure
AWS Access Key ID [None]: [アカウントのアクセスキーID]
AWS Secret Access Key [None]: [アカウントのシークレットアクセスキーID]
Default region name [None]: us-east-1
Default output format [None]: json

さらにAWSのセットアップを行ったが、Failしてしまった。これはどうやって直せばよいのだろう?

$ sudo pip install boto3
$ wget https://raw.githubusercontent.com/firesim/firesim/master/scripts/aws-setup.py
$ python aws-setup.py
Traceback (most recent call last):
  File "aws-setup.py", line 12, in <module>
    avail_zones = list(map(lambda x: x['ZoneName'], client.describe_availability_zones()['AvailabilityZones']))
  File "/usr/local/lib/python2.7/site-packages/botocore/client.py", line 314, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/usr/local/lib/python2.7/site-packages/botocore/client.py", line 612, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (AuthFailure) when calling the DescribeAvailabilityZones operation: AWS was not able to validate the provided access credentials

いろいろ試してみた。ntpupdateをすればよいということで実行してみたが変化なし。

$ sudo emacs /etc/ntp.conf -nw
  • /etc/ntp.conf
# Use and prefer the Amazon Time Service server
# server 169.254.169.123 prefer iburst minpoll 3
server -4 ntp.nict.jp iburst  #おなじものを3つ書く!
server -4 ntp.nict.jp iburst
server -4 ntp.nict.jp iburst

RISC-V SpikeシミュレータでC/C++のprintfを実現する仕組み (5. システムコールの呼び出し)

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

f:id:msyksphinz:20180616132641p:plain

Spikeシミュレータの構造を調べているのだが、printf()などの部分はすべてシステムコールを読んで実現するようになっている。 そして、システムコールの部分はriscv-pk/pk/syscalls.cc で実現されている。HTIFで受け取ったシステムコールをriscv-fesvrで処理している。

github.com

簡単に言うとHTIFで受け取ったシステムコールを、シミュレータに接続されたsystem_proxy関数で処理している。

システムコールはいつ呼ばれるのかというと、Spikeの命令セットを実行するエンジンとは横に、デバイスを動作させている部分があり、この部分がデバイスリストを1サイクル毎にtick()で呼び出している。これにより関数コールが検知されるというわけだ。

int htif_t::run()
{
  start();

  auto enq_func = [](std::queue<reg_t>* q, uint64_t x) { q->push(x); };
  std::queue<reg_t> fromhost_queue;
  std::function<void(reg_t)> fromhost_callback =
    std::bind(enq_func, &fromhost_queue, std::placeholders::_1);

  if (tohost_addr == 0) {
    while (true)
      idle();
  }

  while (!signal_exit && exitcode == 0)
  {
    if (auto tohost = mem.read_uint64(tohost_addr)) {
      mem.write_uint64(tohost_addr, 0);
      command_t cmd(mem, tohost, fromhost_callback);
      device_list.handle_command(cmd);
    } else {
      idle();
    }

    device_list.tick();

    if (!fromhost_queue.empty() && mem.read_uint64(fromhost_addr) == 0) {
      mem.write_uint64(fromhost_addr, fromhost_queue.front());
      fromhost_queue.pop();
    }
  }

  stop();
void htif_t::register_devices()
{
  device_list.register_device(&syscall_proxy);
  device_list.register_device(&bcd);
  for (auto d : dynamic_devices)
    device_list.register_device(d);
}

バイスリストにsyscall_proxybcdが登録されている。このうち、syscall_proxyシステムコールを処理している。 このsyscall_proxyを探ってみると、以下で定義されている。

htif_t::htif_t()
  : mem(this), entry(DRAM_BASE), sig_addr(0), sig_len(0),
    tohost_addr(0), fromhost_addr(0), exitcode(0), stopped(false),
    syscall_proxy(this)  // syscall_proxyを登録
{
...
class htif_t : public chunked_memif_t
{
 public:
...
  syscall_t syscall_proxy;
...
};

syscall_t クラスはシステムコールの関数群をテーブルとして保管している。このtableメンバがシステムコールとして使用する関数を保存しているわけだ。

class syscall_t : public device_t
{
 public:
  syscall_t(htif_t*);

  void set_chroot(const char* where);
  
 private:
  const char* identity() { return "syscall_proxy"; }

  htif_t* htif;
  memif_t* memif;
  std::vector<syscall_func_t> table;
  fds_t fds;
...

システムコール関数は以下のようにテーブルに格納される。

syscall_t::syscall_t(htif_t* htif)
  : htif(htif), memif(&htif->memif()), table(2048)
{
  table[17] = &syscall_t::sys_getcwd;
  table[25] = &syscall_t::sys_fcntl;
  table[34] = &syscall_t::sys_mkdirat;
  table[35] = &syscall_t::sys_unlinkat;
  table[37] = &syscall_t::sys_linkat;
  table[38] = &syscall_t::sys_renameat;
  table[46] = &syscall_t::sys_ftruncate;
  table[48] = &syscall_t::sys_faccessat;
  table[49] = &syscall_t::sys_chdir;
  table[56] = &syscall_t::sys_openat;
  table[57] = &syscall_t::sys_close;
  table[62] = &syscall_t::sys_lseek;
  table[63] = &syscall_t::sys_read;
  table[64] = &syscall_t::sys_write;
  table[67] = &syscall_t::sys_pread;
  table[68] = &syscall_t::sys_pwrite;
  table[79] = &syscall_t::sys_fstatat;
  table[80] = &syscall_t::sys_fstat;
  table[93] = &syscall_t::sys_exit;
  table[1039] = &syscall_t::sys_lstat;
  table[2011] = &syscall_t::sys_getmainvars;
...

ファイルディスクリプタは、同様にhtifで定義されている。fds_tにより、stdin, stdout, stderrが定義される。

syscall_t::syscall_t(htif_t* htif)
  : htif(htif), memif(&htif->memif()), table(2048)
{
...
  int stdin_fd = dup(0), stdout_fd0 = dup(1), stdout_fd1 = dup(1);
  if (stdin_fd < 0 || stdout_fd0 < 0 || stdout_fd1 < 0)
    throw std::runtime_error("could not dup stdin/stdout");

  fds.alloc(stdin_fd); // stdin -> stdin
  fds.alloc(stdout_fd0); // stdout -> stdout
  fds.alloc(stdout_fd1); // stderr -> stdout
...

例えば、printf()を実行するとputs()が実行され、その結果システムコールとしてはsys_writeが呼ばれる。

reg_t syscall_t::sys_write(reg_t fd, reg_t pbuf, reg_t len, reg_t a3, reg_t a4, reg_t a5, reg_t a6)
{
  std::vector<char> buf(len);
  memif->read(pbuf, len, &buf[0]);
  reg_t ret = sysret_errno(write(fds.lookup(fd), &buf[0], len));
  return ret;
}

write(fds.lookup(fd), &buf[0], len) により、stdoutに対して文字が出力されるという構造だ。

RISC-V fesvrを改造して動作を確認

上記の解析を確認するために、riscv-fesvrにstd::coutを挿入して動作を確認してみた。

$ git diff
diff --git a/fesvr/syscall.cc b/fesvr/syscall.cc
index 6e8baf6..d892851 100644
--- a/fesvr/syscall.cc
+++ b/fesvr/syscall.cc
@@ -149,6 +149,11 @@ reg_t syscall_t::sys_pread(reg_t fd, reg_t pbuf, reg_t len, reg_t off, reg_t a4,

 reg_t syscall_t::sys_write(reg_t fd, reg_t pbuf, reg_t len, reg_t a3, reg_t a4, reg_t a5, reg_t a6)
 {
+  std::cout << "<Info: sys_write (" << std::hex << fd << ", " << std::hex << pbuf << ", "
+            << std::hex << len << ", " << std::hex << a3 << ", "
+            << std::hex << a4 << ", " << std::hex << a5 << ", "
+            << std::hex << a6 << ");>\n";
+
   std::vector<char> buf(len);
   memif->read(pbuf, len, &buf[0]);
   reg_t ret = sysret_errno(write(fds.lookup(fd), &buf[0], len));
@@ -346,6 +351,8 @@ void syscall_t::dispatch(reg_t mm)
   if (n >= table.size() || !table[n])
     throw std::runtime_error("bad syscall #" + std::to_string(n));

+  std::cout << "<Info: table[" << std::dec << n << "] is calling>\n";
+
   magicmem[0] = (this->*table[n])(magicmem[1], magicmem[2], magicmem[3], magicmem[4], magicmem[5], magicmem[6], magicmem[7]);

   memif->write(mm, sizeof(magicmem), magicmem);

改めて作ったプログラムを見てみる。

  • cat test_output.c
#include <stdio.h>

int main ()
{
  printf ("Hello World, C\n");

  return 0;
}

実行結果。Hello World, Cを出力するより前に、いくつかシステムコールが呼ばれている。 sys_writeシステムコールでは、stdout に対して文字列を出力していることが分かる。

spike pk test_output_c &> test_output_c.log
$ <Info: table[2011] is calling>
<Info: table[56] is calling>
<Info: table[67] is calling>
<Info: table[67] is calling>
<Info: table[67] is calling>
<Info: table[67] is calling>
<Info: table[67] is calling>
<Info: table[57] is calling>
<Info: table[80] is calling>
<Info: table[64] is calling>
<Info: sys_write (1, 80829030, f, 0, 0, 0, 0);>
Hello World, C
<Info: table[93] is calling>

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

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

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

参考文献

[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;
}