特集

暇すぎるNPUに「DOOM動く?」と聞いたら、Claudeが掛け算の暴力で動かした話

 最新のWindows AI PCの中で、おそらくほとんどのシーンで一番暇を持て余しているプロセッサがNPU(Neural Processing Unit)だ。

 いや失礼、さすがにちょっと言い過ぎた。Copilot+ PCなら、一部の機能でしっかりNPUを使っているし、Windows Studio Effects対応機種などでも、Web会議のときに活躍してくれている。それらの機能を毎日使っているのであれば、NPUは陰で働いている。

 しかし日常的な作業で、果たしてそこまでNPUの機能を使っているのかと言われれば、やはり微妙だ。必須といえるCPUはさておき、Webページを描画したり動画を見たりするだけでGPUは勝手に使われるし、通信をすればネットワークコントローラも使われる。それらと比較してNPUの出番は少なく、使われ方は受動的だ。

 だが、今の時代はAIがある。AIを使えば、ソフトウェアの可能性を押し広げることができ、これまで活躍できなかったハードウェアに新たな活躍の場を与えることができる。どんなプロセッサも、どんなコントローラも、ソフトウェアがあって初めて生きるからだ。なら、AIを使って、もっと積極的にNPUを使うことはできないのだろうか?

ところでそれ、DOOM動きますか?

 日本国内ではあまりそういう文化がないかもしれないが、海外のPCマニア、あるいは半導体業界には「ところでそれ、DOOM動きますか?(But can it run DOOM?)」という文化がある。簡単にいえば、プログラムとプロセッサがあるところにDOOMがあるべきなのだ。

 しかしこのサイトには、キヤノンのカメラやスマートバンド、ESP32搭載デバイスへの移植報告はあれど、それらよりもずっと高性能な演算能力を持つNPUへの移植報告は、筆者が確認した限り見当たらない。

 ならば、暇しているNPUを能動的に動かしてみようか。筆者はそう考えた。

 と、言ったものの、実はDOOMを動かせるNPUは、この世には存在する。それがQuadricのNPUだ。

 Quadricのブログによれば、普通のNPU(AIアクセラレータ)はDMAエンジンに接続されたシストリックアレイで、行列演算に特化したプロセッサだ。一方、DOOMはその行列演算が皆無で、使う演算は加算、減算、シフト、ビット演算、比較、テーブル参照、スカラー整数乗算、固定小数点演算のみ。そのためDOOMの動作にまったく適していない。

言わずと知れたPCの3Dゲームレジェンド、「DOOM」

 それ以前の問題として、ほかのNPUで走らせることができないとQuadricが断言している理由は以下だ。

1.視線ごとに止まる場所が異なる

 3Dゲームの描画は、画面の縦1列ごとに「視線」を伸ばし、最初にぶつかった壁を描いていく仕組みだ(ちなみに元祖DOOMは視線ではなく、BSPというツリー構造をたどる方式)。近くの壁なら2ステップ、遠くの壁なら20ステップといった具合で、何ステップで終わるかはやってみないと分からない。CPUはステップごとに壁に当たったかどうか確かめられるが、NPUは当たったら止まるという命令がなく、すべての演算ユニットが同じステップを歩くしかない。

2.1点ごとの細かい判断

 どちらの壁が手前か、この点は透明なのか、上書きをするのか……など、描画は画素ごとの小さな判断の連続である。行列の掛け算を流し込むNPUには、こうした1個ずつの判断をする回路がない。

3.前の結果を待ってから進む

 列ごとの下調べが終わってからその結果を使って描く。つまり、前段の答えが出ないと後段処理に進めない。ユニット全体が同時に同じことをするNPUには、この順番待ちの仕組みがない。

4.テクスチャの取得がその場で決まる

 壁などに貼るテクスチャは、「この壁のこの位置」に対応する色を表から引く仕組みとなっているが、その表のどこを引くかは視線がどこに当たったかごとに毎回変わる。NPUは隣り合ったデータを決まった順番で運ぶようになっているが、計算で決めた番号の場所から1個だけ取得する仕組みを持たない。

 ざっくり言えば、NPUは膨大な演算リソースを持っているが、演算ユニットは決められた演算しか行なえず、分岐を判断してから動作する仕組みがないためDOOMは動かせない、というわけだ。

 一方、同社のGPNPU(汎用NPU)には整数のALUや分岐命令が用意されており、計算で決めた番地から1個だけ読む回路や、順番待ちの仕組みも備えている。DOOM風の描画エンジンを自前実装して動作させた、と報告している。

 QuadricのGPNPUはDOOMが動いてすごい、というのは分かったのだが、当然筆者の手元にはそのGPNPUがあるわけではないし(しかもブログで紹介されているのはFPGA上で動作するプロトタイプ)、そもそも動かしたいのはIntel CPUに内蔵されているNPUだ。

 だが、今はAI(筆者の場合はClaude Code)がある。その力にすがってみることにしよう。ちなみに今回のプロジェクトスタート時に使ったモデルはClaudeの「Fable 5」だ。

Claude Code: いいでしょう。ただしエンジンは自前実装

 実はDOOMをNPUに載せる前に、NPUで動作するテキストエディタを作らせたのだが、ものの20分で完成してしまった。そこで「テキストエディタもできたんだからDOOMも移植できるよね。当たり前だよねいくらでも演算リソースとメモリあるんだもの。完成までやってください」とClaude Codeに丸投げした。

 ただ先述の通り、DOOMのオリジナルエンジンをそのままNPUに移植するのは無理だったようで、「自前実装で行きます」といきなり宣言してきた。時間がかかりそうな作業なので、その日はPCをつけたまま寝ることにした。

 翌日起きてみたら、PCの画面にはDOOMが起動していた。Claude Codeのログを見てみたが、筆者にはよく分からない言葉で埋め尽くされていた。しかも途中で2回も5時間の利用制限に引っかかっていたようだ。幸い、直前に実装された、上限に当たった後に自動で継続する機能により、無事完成したのだが。

筆者にはよく分からない言葉が並べられているが……
翌朝に起きてみたら起動していた

 では、Claude Codeはいかにして、Quadricのブログで挙げていたNPUでDOOMを動かす障壁を乗り越えたのだろうか?

「もし~なら~」を計算式にする

 その一番の鍵となるのは、「もし~なら~」(つまりIF構文)を書き直すことにある。先述の4つのポイントのうち1と2はこの一点に集約される。というより、それ以外にもゲーム性に関わるすべてのルーチンがIFだといってもいい。

 簡単な例でいえば、「“もし”弾が当たった“なら”体力-10」だ。それをClaude CodeはNPU向けに以下のように、掛け算に書き直したのだ。

 体力=体力-10×当たり判定

 当たり判定は、当たれば1、当たらなければ0。つまりただの計算となり、当たらなければ10×0=0が引かれるが体力はそのまま、当たれば10減る。シャア・アズナブルの言葉を借りれば「当たらなければどうということはない」わけである。そしてこの計算を(面倒だが)毎回必ず実行するようにすれば、その結果はIF文で判断したのと同じになる。

Claudeのアーティファクトを使って、「もし~なら~」を使わないDOOMを解説させたところ

 この手法だと、分かれ道のたび両方計算していては、計算量がとんでもないことになりそうなイメージなのだが、実は分かれ道の片方に1を、もう片方に0を掛けてその場で足していくので爆発的に増えない。DOOMでは1フレームあたり分岐が3,337カ所あるが、計算量はその2倍(0か1か)なので6,674回分で済む。

 この手口を、Quadricが挙げた4つの障壁に当てはめていくと、以下のようになる。

1.視線ごとに止まる場所が異なる

 「視線が壁に当たったら止まる」という判断ができないなら、止まらずに一律壁を8枚くぐるまで進ませて、壁に当たった列に対して以降の計算結果に0を掛け続けて黙らせる。1段ごとに160本(横幅160ドット)の視線×475枚の壁=7万6,000通りの「この視線はこの壁に当たるか」を全部計算して、視線ごとに一番近い壁だけを残す。

 1フレームで描画される壁は多くても数十枚(最大37枚、中央値8枚)なので、この手法では計算の99%以上が捨てられることになる。つまり、NPUの演算能力の高さは、この無駄の犠牲となってしまった……。

視線を一気に飛ばし、壁に当たった時点で0を返してマスクする

2.1点ごとの細かい判断

 これは上記と同様、全画素で両方を計算しておいて0か1で選ぶ方式とした。ただ、Intel NPUの演算で最小値/最大値を求める命令は、2,048を超える値で結果が壊れ、わり算は正確な答えを返さないという。そこでClaude Codeは最小値と最大値は「比較」と「選択」の組み合わせで求め、わり算は256進数の筆算に置き換えたという。

3.前の結果を待ってから進む

 NPUにはループ命令がないが、先述の通り1フレームの中での処理数は決められている。前段の結果を受けての後段の処理を1本の“シナリオ”の中に順番に並べるだけで成立はする。その結果、シナリオは1万数千個の計算命令が一列に並んだものとなったが、NPUはこのシナリオに沿って毎回最初から最後まで実行する。

1フレームの中で起こる分岐を全部計算して不要なものに0を掛けて無効化し、1本道に合流させる

4.テクスチャの取得がその場で決まる

 これはQuadricが定義した「普通のNPU」からIntelのNPUが外れていた部分。というのもIntelのNPU向け実行環境(OpenVINO)には、番号を指定して表から値を取り出す「Gather」という演算があり、これがそのままNPU上で実行できるからだ。行列演算に比べると実行は遥かに遅いが、「ない」わけではなかった。

 仮にGatherがなかったとしても抜け道はあり、先ほどと同様、表の全項目に「その番号なら1、違ってたら0」を掛けて全部足せば、原理的には動く。とはいえテクスチャの色を1個取得するために4,096回の掛け算をするのでとてつもなく遅くなるそうだ。

「完全にNPU」は無理

 描画やゲーム内処理をNPUに任せることに成功したが、結局どうしてもCPUの手を借りなければならない処理は残る。

 まず1つ目はゲームのループだ。1フレーム単位では処理の上限が決まっているので、ループ命令がないNPUでも処理はできるし、終了の判断も出力できる。しかし、次のフレームを自分で始められない。出力を次の入力に戻して繰り返す仕事だけがCPUに残った。

 もう1つは(当然ではあるのだが)キーボードからの入力を受け付けるところと、画面に出力するところ。そもそもCPUが司っているので、NPUが横から仕事を奪えるとか、そういう次元の話ではない。それでも、ゲームの描画ルーチンや判定ルーチン自体、NPUに入れ込めた。

ちなみに当初、背景がなぜか逆回転する、ドラムがついてくる、敵キャラがこっちを向かないといったバグがあったが、全部修正された

速度は犠牲になったが、NPUでDOOMは動いた

 原理的な話で長くなってしまったが、「1フレームあたり1万数千の演算、大半はNPUにやさしくない演算」でどうにかDOOMは動作した。ちなみに寝ている間に最初に完成したバージョンは0.65fps程度でまったくプレイに耐えないレベルだったようだ。朝起動していたバージョンでは4.4fpsだったが、これはネイティブの320×200ドットから160×100ドットに落としていた。その後あれこれ手法を試したり、「Fable 5.1」がリリースされてからUltracodeを使って突き詰めていったが、9fps前後が限界となった。

 ちなみに、これはArrow LakeことCore Ultra 9 285HXのNPUでの結果だが、Lunar LakeのCore Ultra 7 268Vでもほとんど高速化しなかった。NPUが得意な並列処理にできる部分は並列化しているが、そもそもDOOMは並列化がなかった時代のゲームで、シリアル処理が大前提。しかもNPUはメモリアクセスが遅く、計算の大半は0を掛けられて捨てられるという非効率極まりない処理なのだから仕方ない。

 しかし筆者が求めたのは速度や効率ではない。NPUを能動的に使う可能性だ。そもそもQuadricが「試みることすらできない」と断じたプロセッサで、DOOMを走らせた。「分岐ができないのなら、分岐の両方を計算して不要になったものを捨てる」という大胆な発想ができるAIが、その可能性を見せてくれたのだ。

 NPUに限らず、今読者の手元にも「積んであるのに使われていない」演算資源があるのではないだろうか。それを引き出すのに必要なのは新しいハードではなく、ハードの制約の中で活用法を考え直せる相手だった。手元の余っている力を、AIと一緒に使ってみてほしい。

 もっとも、今回の実験を経てそれ以上に「NPUでDOOMを動かすのは演算資源の無駄である」ということがよく分かった。だがそれが分かったのも、AIが命令を1つずつ実測して「なぜ遅いか」を数字で示して、仕組みを解説してくれたからだ。ハードウェアの特性を理解するうえでもAIは有用なのだと、改めて思うのであった。

超強力なNPUは巨大なトラックなのだが、結局それは「同じ目的地に行くなら速い」だけで、今回のDOOMのような処理はほとんどが無駄(しかも大半が捨てられている)

 末筆ながら、本記事でのDOOMのスクリーンショットの使用を快諾いただいたBethesda Softworksに、この場を借りて感謝申し上げたい。