GPUを使って無線LANを(略) 番外編 GPUなしでCUDAは動かせるのか?

紅葉への切り替わり中です。秋って感じですね。

でも町を歩いていると、8月末くらいには紅葉のポスターが貼られだして、ハロウィン一色になったなぁ、と思ったら、今はもうクリスマス一色です。町から紅葉の絵はなくなり、雪の結晶やクリスマスツリーの絵ばかり。気が早えぇなぁ、人間。

わたしは最近こういう「イメージ」も「拡張現実」の一種なんじゃ無いかと感じています。ARゴーグルだけが「拡張現実」では、ないんじゃない?

みなさんはどう思います?

…と聞いてみるにもコメント欄ないんだったわ、このブログ。トラックバックもない。…時代は変わってしまったなぁ。

GPUサーバがない!

というわけで、前回山の上で瞑想して考えた「ぼくの考えた最強のベンチマーク・ルール」にしたがってガンガンPyritを書き換えていきたいところ…なのですが、なんと今回はオトナの事情により、GPUサーバは使えないらしいのです。かなしい。

しかし、案ずることなかれ。

こんな事もあろうかと、もう2枚ほどGeForce 1080を用意しておきました。

これをどこのご家庭にもある、PCI-Express用の電源ポートがなぜか10個あってPCI-Express x16のスロットがなぜか4つあるデスクトップパソコンに刺してPyritの改造をしていきましょう。

がっちゃん(GPUを突き刺す音)

カチッ(電源を押す音)

ブワー(電源がつきファンが回る音)

ピカピカピカ(1680万色に光るゲーミングマザーボードのLED)

…(しかしモノリスのように真っ黒なまま沈黙するモニタ)

…(その前でなす術もなく立ち尽して一緒に沈黙する筆者)

…(なす術もなく立ち尽くす筆者)

……(立ち尽くす筆者)

…………

……………はぁ〜〜〜〜〜〜〜

なんで画面が出ないんだよ〜〜〜〜〜〜〜〜

グラフィック・ボード」じゃなかったのかよ〜〜〜〜〜〜〜

…嘆いていてもしょうがないので、GPUなしでなんとかする方法を考えましょう。

遅くてよいので、CPUだけでCUDAのコードを動かせないものか…。

原理的には可能なはずですよね。CUDAはただ計算が早くなったり(ならなかったり)するだけの技術ですから。同じPCIeの拡張カードでも、真の乱数発生ボードと違って、原理的にはエミュレーションできる事は明らかです。原理的には、だけどね。

わたくし結構macでノマドもするので、できればLinuxだけじゃなくてmacでも動いて欲しいなぁ(欲を出しておく)。

あーそうそう、画面が出ない理由ですが、Radeonと一緒に刺すとダメみたいです。グラボのバグなのか、Linuxのバグなのか、その辺は、よくわからない。

代案を調べる

代案 その1:gpuocelot

gpuocelot公式サイトより引用

gpuocelotは、CUDAの実行ファイルであるPTXをnVidiaのGPUだけでなく、AMDのGPUやCPUで直接実行したり、エミュレーションしたり、ネットワーク経由で他のマシンのGPU(やCPU)で実行したりできるようにするぜ!というそのものズバリわたしが求めていたソフトウェアです。

が、公式サイトいわく「The GPU Ocelot project is no longer actively maintained.」ということで開発終了。まぁ大学製ソフトなので、資金が尽きたのか論文書き尽くしたのか、まぁ、そんな所でしょうか。しかも最後の論文が2014年なのであまり期待はできなさそうです。

あと個人的なプログラマ、あるいは大学院生しての直感が告げているのですが、こんな野心的なソフト、本当に安定して動くのかな…論文書くための機能が一応動いてるだけじゃないかな…という疑念が正直あります。

はい次。

代案 その2: MCUDA

The MCUDA translation framework is a linux-based tool designed to effectively compile the CUDA programming model to a CPU architecture.

IMPACT: MCUDA Toolset

こちらも大学製。何か書くにも、一切ドキュメントがない。

はい次。

代案 その3:CUDA Waste

GPUocelotはLinux専用らしく(gpuocelotのページにはそんな事書いてなかったけどな!)、そのためにWindows用に同じようなソフトウェアを開発したのがこちらだそうです。こちらもだいぶ前から更新がないので実用は難しそう。あとわたしWindows持ってない。

はい次。

代案その4: CU2CL

CUDAのソースコードをOpenCLのソースコードに変換するというソフトウェア。今までのソフトウェアに比べると大分「現実的」な落とし所なような気はします。

が、実際にこれを使おうとすると、CUDAを実行していた部分をOpenCLを実行するためのコードにモリモリ書き換えないといけませんし、そもそもPyritにはOpenCLのコードは最初から入っています。

悪くは無いんだけど、今回の場合は色々な意味で本末転倒。

はい次。

代案その5: NVEmulate

これはなんとNVIDIA公式。古いGPUのマシンでも、新しいGPUを要求するソフトウェアを動かせるようにするためのソフトウェアだそうです。しかし2014年から更新がなく、ついでに言えばWindows専用。よしんばWindowsを持っていたとして、CUDAが動くかどうかは怪しいです。ゲーム開発者のデバッグ用っぽい。

「非常に低速」であることには違いないが,例えば,GeForce FXシリーズのマシンでGeForce 6800のデモであるNaluやTimburyが動いてしまうという素晴らしいツールだ(普通の人用には,これ以外の用途を思いつかないのが残念だが)。
手持ちのGeForce FX 5900搭載マシンでは,Naluが0.1fps程度で動いている。

4Gamer.net ― 君のマシンでNaluが動く! GPUエミュレータNVemulate公開

はい次。

代案その6: nvcc -deviceemu

nvcc –deviceemu <filename>.cu

  • デバイスエミュレーションモードでビルド
  • デバッグシンボルなし、CPUですべてのコードを実行

NVIDIA – CUDA実践エクササイズ

こちらも公式。CUDAコンパイラであるnvccが、CUDAソースコードのうち、GPUで走るはずの部分も全部CPUで走るようにコンパイルしてくれるんだそうな。

これはかなり理想的かつ現実的なアプローチに思えます。PyritのCUDAモジュールをコンパイルする時にこのオプションをつけておくだけでいいのでラクチンですし、mac用のnvccもあるのでノマドワーカーにも優しい。そしてPTXを動的に変換するような大道芸はしない、安定したアプローチ。

しかし

 % cd /Developer/NVIDIA/CUDA-9.2/bin
 % ./nvcc --help | grep deviceemu
 %

とっくの昔にそんなものは無くなっていたのであった。

CUDA Emulation Modeというのもあった(GPUの代わりにCPUのエミュレータが出てくる)というのも昔はあったようなのですが、これも今はもうなさそう。

NVIDIAからの「GPU買ってね!」という熱いメッセージだと思っていいのかな…。でもそれ以前に動かないんだけど。

gpuocelotを試す

現実的に使えそうなものはgpuocelotぐらいなので、これちょっと試してみましょう。Instlationページによると、WindowsはExperimental(もう二度とExperimentalが外れることはなさそうだけど)で、macは最初から言及すらないので、ノマドは諦めてLinuxでやりましょう。

Ubuntu 18.04でやる事にします。

ビルド・インストラクションに沿って(かつGCC4.8では動かないなどの諸々の注意書きを見なかった事にして)やってみると:

ocelot/ir/implementation/Module.cpp:712:46: error: enum constant in boolean context [-Werror=int-in-bool-context]
     if (prototypeState == PS_ReturnParams || PS_Params) {
                                              ^~~~~~~~~
cc1plus: all warnings being treated as errors
scons: *** [.release_build/ocelot/ir/implementation/Module.os] Error 1
Build failed...
Build failed

「PS_Paramsはenumなのでboolの値としては使えないんだけど…!」という、もっともなエラー。しかしどう直せばいいのか。||の前を見る限り、”prototypeState == “を忘れたんですかね?

…これ…コンパイルエラーじゃなかったら、常に”true”になるif文としてコンパイルされてたんじゃないかな…。それでいいのかな…この世はコワイネ。

とりあえず追加してビルド再開。

In file included from ./ocelot/parser/interface/PTXLexer.h:11:0,
                 from ./ocelot/parser/interface/PTXParser.h:16,
                 from ocelot/ir/implementation/Module.cpp:10:
.release_build/ptxgrammar.hpp:354:22: error: 'PTXLexer' is not a member of 'parser'
 int yyparse (parser::PTXLexer& lexer, parser::PTXParser::State& state);
                      ^~~~~~~~
...(略)...
.release_build/ptxgrammar.hpp:354:47: error: 'parser::PTXParser' has not been declared
 int yyparse (parser::PTXLexer& lexer, parser::PTXParser::State& state);
                                               ^~~~~~~~~
...(略)...

scons: *** [.release_build/ocelot/ir/implementation/Module.os] Error 1
Build failed...
Build failed

parser::PTXLexerや、parser::PTXParser::Stateを宣言していないのに、yyparse関数のパラメータとして使ってるぞ!という、これまたもっともなコンパイルエラー。

ptxgrammar.hppはyaccから生成されるファイルで、NVIDIAのGPUの命令セット、PTXのパーサーのようです。で、このパーサーがparse::PTXParser::Stateを利用すると。

で、PTXParserを見ると次のようになっていて、プログラムの他の部分から使うための外面の実装のようです。そのため、ptxgrammar.hppにガッツリ依存しております。

namespace parser
{
	/*! brief An implementation of the Parser interface for PTX */
	class PTXParser : public Parser
	{
		public:
			class State
			{
				public:
					class OperandWrapper
...(略)...

必然的に、循環参照が発生しています。

PTXParserはyaccの生成したコードに依存していて、yaccの生成したコードはPTXParser::Stateに依存している。

で、コンパイラは、どちらを先に読み込んだとしても、知らないクラスや知らない定数がいきなり出てくるので、よくわかんなくなって、困ってしまう、と。

こういうシチュエーションはC++ではよくあることで、そのためにクラスの前方宣言という機能が用意されております:

namespace hoge{
class Fuga; // これが前方宣言。
}

// ポインタ、もしくは参照としてなら使える
int foo(hoge::Fuga& fuga);

namespace hoge {
// 実際の宣言
class Fuga {
...(略)...
}
}

こんな感じで最初に「hogeというネームスペースの中にFugaというクラスがあるぞ!」とコンパイラに教えておいてあげると、foo関数の定義を見たコンパイラは、「よくわかんないけど、そういうクラスがあるんやな」となんとなく察してコンパイルを続行することが出来ます(逆に、前方宣言をしないと「hoge::Fugaって何?」とエラーを吐いて止まってしまいます)。ただ、あくまで「なんとなく」なので、ポインタか参照としてでしか使えません(もっと詳しくはコンパイラの本かC++の本を読んでね)。

じゃあ今回もそうするか…と言いたいところなのですが、parser::PTXLexerはともかく、parser::PTXParser::Stateクラスはふつうのクラスではありません。上で見たとおり、Parserクラスの中に存在するクラス、「インナークラス」です。C++では、残念ながらインナークラスの前方宣言は出来ません。もしできれば万事解決しそうなのですが、C++の仕様上できないので仕方がない(どうしてそういう仕様なのかは、謎)。

まぁ、このインナークラスが前方宣言できなくて困るシチュエーションも、C++ではよくあることです。じゃあ、そういう時はどうするのかというと、普通に設計を見直してリファクターします

…はぁ…。まぁ、「動かない」ってことで、いいか…。でも、なんで今までこれでコンパイル通ってたんだろう?

今回のまとめと次回のPyrit

GPUなしでCUDAの実行をするのは、現実的にはどうやら難しいらしいことがわかりました。

CUDA用の純正コンパイラ「nvcc」に、CPUでエミュレートするためのコンパイルフラグが、過去には存在していたようですが、現在は跡形もなく無くなってしまいました。CUDAの使えるGPUが非常に限られていた時代ならいざしらず、NVIDIAのカードならどれでもCUDAが使える昨今ではあんまり需要は無くなったからなのか、…それともCUDAがGPGPUとしての覇権を取ったから、傲慢になったのか。その辺はわかりませんが。

ちなみにTensorFlowとかCaffeはCPU用とGPU用のコードが両方ともゴリゴリ書いてあったと記憶しております。大変だねぇ。資本を上げて人月で殴れ!

次回の月刊Pyritの頃にはGPUサーバが使えるようになるはずなので、素直にGPU使って開発しようと思います。