Calendarの16日目の記事です。日付と投稿日がかみ合っていなくてすみません。概要PEZY-SCというメニーコアプロセッサーでRustのコードを動かしてみたというお話です。Computingが開発したメニーコアプロセッサーです。1024コアのRISCプロセッサーで、各コア8スレッドのSMTになっており、トータルで8192スレッドが同時に動きます。ピーク性能は倍制度1.5TFlops、単精度3TFlopsで、これを用いたシステムが電力効率の良いスパコンとしてGreen500などで良い成績を収めています。現在さらに性能を向上させたPEZY-SC2を開発中です。高い演算スループットと電力効率を目指しながらも、SIMDを用いない完全なMIMDプロセッサーで、ある意味コンピューター科学の常識に反しているようなユニークな設計だと思います。現在のところは残念ながら広く一般に利用できる状況にはありません。いうかこんな記事を書いておきながら言うのもなんですが、この記事を見てくださっているほとんどの人は使ってみるすべがないと思います。申し込んでいただくと、もしかしたら使えるかもしれません。現状のPEZY-SCのプログラム開発環境OpenCLのAPIのようなAPIが用意されており、それでホストとデバイス向けのコードを書いて実行します。ホストコードは一般的なOpenCLのプログラムと同様に、デバイスオブジェクトやプログラムオブジェクトの作成を行って、デバイスのカーネル関数を起動します。Cではなく、普通のC++で記述します。言語としては普通のC++が使えますが、ランタイムのサポートがないので、標準ライブラリの大部分は使えません。いえ、テンプレート関数やラムダ式などは問題無く使えますし、関数の再帰呼び出しやif文などもGPGPUとは違い気兼ねなく使うことができるので、かなり普通のプログラミングと同じような感覚でプログラムを書くことができます。PEZY-SCでRustを動かす方針さて、いよいよ本題のRustですが、実はもうすでに動かすための要素は世の中に存在しているので、それらを適当に繋げるだけで動いてくれるはずです。PEZY-SCのSDKでは、コード生成をLLVMのバックエンドとして実装しています。フロントエンドとしてClangを使っているので、それでC++のコードからPEZY-SC向けにコンパイルできるという形です。いうことは、RustからLLVMのIRなりBitcodeなりを出力できれば、それをバックエンドに食わせて、PEZY-SCのネイティブコードが出力できるはずです。都合が良いことに、RustはバックエンドとしてLLVMを用いています。コンパイラにLLVMのIRやBitcodeを出力するオプションもあります。PEZY-SCのネイティブコンパイラが完成します。世の中便利になったものです。通常はコンパイルしたコードを動かそうと思うと、各言語処理系のランタイムを移植する必要が出てきますが、これも非常にありがたいことに、Rustはとてもランタイムが小さい言語になっています。PEZY-SCで動かすにあたって特にうれしい点として、GCが無いというのがあります。GCがないので、当たり前ですがGCのコードを移植する手間が無くなり、また、処理系のためのメモリ管理のコードも必須ではなくなります。それに加えて、RustはCの代替を本気で狙いに行っている言語なので(?)、標準ライブラリを無効にして、ランタイムへの依存を極限にまで小さくするオプションが用意されています。これを利用すると、Rustを動かすために必要なランタイムはごく少数の関数のみになります。言え、こうすれば理屈の上では動くはずだ、というのと、実際に動く、との間にはものすごく大きな違いがあって、動くはずのものがなぜかすんなりとは動かないというのが世の常ですから、実際にお手軽にできるのかやってみる必要があります。実際にお手軽にできたら、Rustの理想は本当なんだと改めて実感することができるでしょう。IRにコンパイルするまずは小さいコードで、コンパイルできる環境を作っていきます。C++で適当なPEZY-SCのコードを用意します。flush();}与えられた引数を足し合わせるだけの、取るに足らないコードです。これをRustで書いてみます。flush()というのは、メモリキャッシュのフラッシュを行います。PEZY-SCではプログラム終了時にキャッシュの中身が書き出されないので、明示的に呼んでやる必要があります。PEZY-SCのSDKでは、ホストコードから起動される関数の名前の先頭はpzc_で始まるというのを期待しているので、それに合わせてやります。flush();}ほとんど直接的に翻訳できるのですが、Rustではポインタを扱う操作は自動的にunsafeになるので、そこをunsafeで囲んでやる必要があります。flush();}このようになります。#[no_mangle]は、関数名のmanglingを行わないように指示するRustのプラグマです。このままではflush()が無いといってコンパイルがこけるので、とりあえず適当に定義しておきます。余計な依存を発生させないように#![no_std]をつけておきます。flush();}これをrustcでコンパイルします。test.rs--crate-type=libをつけるとライブラリとしてコンパイルされるようになります。これがなければmain関数がないといってコンパイルがこけます。Rustには#![no_main]というプラグマもありますが、その場合は使われていない関数(pubが付いているものも含む)の削除などの最適化が行われるので、やはりライブラリモードとしてコンパイルする必要があります。-Oで最適化オプションを有効にしています。コンパイルすると、次のような結果が得られます。}ターゲットトリプルが“x86_64-unknown-linux-gnu”になっていたり、データレイアウトがそれに合わせたものになっていたりしますが、rustcのバックエンドには当然PEZY-SCのサポートなんてものは存在しないので、見なかったことにします。PEZY-SCで実行するにあたっては、とりあえずはポインタサイズが64ビットでリトルエンディアンになっていれば問題がないはずです。IRをLLVMのBitcodeにします。rustcで直接Bitcodeを出させたほうが速いような気もしますが、今回はRustcとPEZYのSDKが利用するLLVMのバージョンがかみ合わず、うまく通らなかったので、テキストで表現されるのIR経由でBitcodeを作ることにしています。test.bcここではPEZY-SC付属のllvm-linkを使います。これで生成されるBitcodeがSDKの想定するバージョンで生成されたものになります。つぎに、PEZY-SCのランタイムとリンクします。デバイス関数の起動時に実行されるスタートアップルーチンなどが含まれています。kernel.bcつぎにBitcodeをPEZY-SCのアセンブリにコンパイルします。kernel.bcこれでアセンブリまでできました。.func_end16-_Z7pzc_addここまでくれば、あとはSDKの所定の方法でアセンブルしてリンクすればデバイスコードのバイナリができます。kernel.hexIntrinsic関数を作る一応バイナリを生成できましたが、flush()関数が空なので、このままでは正しく動きません。いってもそんなに難しいわけではなく、インラインアセンブリで必要な命令を呼び出してやるだけです。}}Rustではasm!マクロによってインラインアセンブリを書くことができます。この機能は現在のところunstableなので、nightlyバージョンのrustcでしか使えません。nightlyバージョンを導入する必要があって少しめんどくさそうですが、rustupを使えば割と簡単に導入できます。nightlyこれでデフォルトがnightlyに切り替わります。volatileに相当するものを書いたりするために、制約の4つ目の部分が追加されています。volatile扱いにしています。これはキャッシュのフラッシュの指示をメモリの操作と入れ替えられないようにするためです。2}命令が埋め込まれているのがわかります。適当にホストから呼び出すコードを書いて実行します。́・_・`)......。579というわけで無事実行できました!並列化するミニマムなコードを動かすことに成功したので、もうちょっと複雑なコードを動かしていきます。そもそも先ほどのコードはメニーコアプロセッサーなのに全く並列に動かす意味のないコードでした。実際のところ、一応8192スレッドが起動して動いていたのですが、すべてのスレッドが全く同じポインタに、よってたかって同じ値を書き込むという、よくわからないことをするプログラムになっていました(複数のスレッドが同時に同じメモリに書き込む場合でも、それが同じ値なら安全であるし結果は決定的であるという保障はあります)。Worldに相当するものとして、ベクトルの加算を書いてみます。b[i]を計算するという簡単なプログラムです。配列の各要素について計算に独立性があるので、並列に計算してやることができます。要素を計算することにしました。どのスレッドがどこの値を計算すればもっとも効率よく実行されるのかというのは、こういうとても単純な計算でも、プロセッサーのアーキテクチャやメモリ階層などによって変わってくるので、意外と悩ましい部分です。ここで各スレッドは、自分が何番目のスレッドなのか、全体ではいくつのスレッドが走っているのかを知る必要が出てきます。get_pid()やget_maxpid()がそのための関数です。pidはプロセスIDで、自分を実行しているプロセスの番号が返ります。PEZY-SCでは最大1024個のコアが使えるので、0から1023までの値を取ります。tidはスレッドIDで、各コアの中で何番目のスレッドなのかを返します。各コア8スレッドのSMTなので0から7の値を取ります。それぞれ取得するニーモニックがあるので、flush()と同様にこれもインラインアセンブリを書いてやればOKです。ret}今回は返り値があるので、ローカル変数を定義して、それをインラインアセンブリに渡して、結果として関数から返します。ret}これでコンパイルが通ります。void}それっぽいものが出てきました。いうのがありますが、これはどうやらエラーハンドラーに使われるもののようです。referenceと言われた関数だけ定義してコンパイルを通すようにしました。使うコードにコンパイルされています。cを計算する命令です。適当にホストプログラムを書いて実行します。01M要素のデータに対して正しく並列に計算されたようです。大きめのコード:行列の乗算では最後に、もう少しちゃんと役に立ちそうなコードとして、行列の乗算を書いてみます。行列の乗算は非常に簡単な計算ですが、これが最も一般受けする(?)スパコンの性能ランキングTop500のベンチマークでかなり時間的に支配的な処理になっていたりするので、実際想像以上に重要だったりします。BLASという歴史ある線形代数ライブラリでの関数名からdgemmと呼ばれることもあります。}}このように非常に単純に書けるものですが、並列にきちんと性能が出るように書こうとすると、途端にものすごくややこしくなってしまいます。書いたコードを示しておきますが、内容についての解説はこの記事の趣旨の範囲を超えるので(またPEZY-SCのアーキテクチャに大きく依存するため)割愛します。flush();}ホストコードを書いて実行します。...}ここでは行列サイズ2048×2048の乗算を行っています。行われる浮動小数点演算の数は、行列サイズをNとすると、2*N^3回なので、これを所要時間で割れば実効性能が得られます。7.1111e-082048×2048の行列の乗算にかかった時間がおよそ16.65ミリ秒ほどで、実効性能は1031.67GFlopsと出ました。定格でのPEZY-SCの倍精度演算のピーク値が1.5TFlopsですので、これはなかなか悪くない値です。実際ベースにしたC++版のコードでも1.1TFlopsに達しない程度ですので、Rust化による性能低下はごくわずか、あるいはRust版のコードもきちんと生成されるアセンブリコードを見てチューニングすれば、ほとんどないものとして扱えるようになるのではないかと思います。まとめ・所感というわけで、RustのコードをPEZY-SCというプロセッサーで動かすことに成功しました。Rustの処理系はバックエンドにLLVMを用いており、PEZY-SCの処理系はLLVMのバックエンドとして実装されているので、理屈の上では、これらを繋いでやるだけでRustのコードがPEZY-SCになるはずで、実際にやってみたところほとんどその理屈の通りにコンパイルして実行することが出来ました。理屈の上では上手く行くはずのものでも、実際にすんなりと動いてくれたことは、ある意味では予想に反していたというか、Rustって素晴らしいという認識を新たにしました。いうわけで、皆さんもRustをいろいろな環境で動かして遊んでみてください。組み込み等でもRustのようなリッチな型を持つ言語を使えると言うことは、きっと新しい可能性をもたらしてくれるはずです。Bootswatch.。