LLMの生成先としてのMFGの課題と可能性(その3)
LLMに向いた環境を作る大切さとMFGのフィルタという切り口の評価
LLMのコード生成には、向いた分野と向いてない分野があるのは明らかに思う。 これを一段進めると、新しい分野を設計する時には向いているように分野を設計する、 という考えがあるだろう。
今回自分はMFGというプログラム言語を作った。このプログラム言語を作る、というのは、 こうしたLLMに適した分野を設計する際の、重要な選択肢となりうるな、と思った。
MFGはLLMの隆盛よりも前から開発していた言語だし、LLM向けに作ってもいない。 だがたまたまLLMに向いた側面が結構あるな、と気づいたので、そういう話を見ていく事で、 MFGの変わった側面からの紹介と、LLM時代にプログラマが用意するべき環境という事を考えるケーススタディになっているんじゃないかと思って書いてみる。
言語で出来る範囲を含めた言語設計
プログラム言語の設計というと、普通はシンタックスとセマンティクスを指す事が多いと思う。 これは暗黙にはアセンブリより先を言語の外側の世界という事にして、 言語設計とはアセンブリまでの事だ、とみなしたいという思いが背後にある。
だが現実的には言語を作る時には、実行時にどのような実行モデルであるか、 つまりランタイムなども含めたものも言語設計の一部であると思う。 特にLLM向けに言語を設計するなら、このランタイムまで含めた、実行時にどうあるかまでが言語設計に含まれると思う。
MFGを例にすると、MFGはシェーダーの言葉で言うなら「シェーダー+ホスト」がランタイムとなる。 ホストが含まれているので、シェーダーにコンパイル出来る訳では無い(一部はシェーダーにコンパイルするが)。 このどこを言語の記述範囲に含めるか、というのはLLM向けの言語を設計する時にはすごく重要な選択となるのを以下いくつか見ていく事になるはず。
MFGはホストを含むのだけれど、ホストで動く制御フローなどは存在しない。 ホストはIRを解析して限定された形でのみ動く。 だからコードを読まなくても、ある限定的な事しかしない事が保証されている。
MFG以外の例で言うなら、CSSとJavaScriptがある。 CSSをLLMで生成させる事を考えるのは、JavaScriptを生成させる事に比べて、影響範囲が予想しやすいだろう。 例え生成されたコードを見なくても、CSSで出来る範囲はJavaScriptよりもずっと少ない。 CSSは見た目だけのくせに高機能なので色々抜け穴もあるかもしれないが、 言語を設計する比喩としてはCSS的なものの方が、生成物を見なくても済む度合いは高いだろう。
これは制御構造が無い方がいいとかdeclarativeな方がいいという話ではなく、 言語の適用範囲が見た目に限定されている、という話だ。
LLM向けに新しい言語を設計する時には、その実行環境まで含めてその言語が何をするのか、逆に何を「しない」のか、 という事は重要なポイントになる。
単体で閉じた機能を提供するのが望ましい
MFGは、そのファイル一つでフィルターという独立した機能を提供出来る。 これはLLMでの試行錯誤には望ましい。
複数のファイルをいじるのは対話的に変更していくのがやりにくいし、 一つのファイルに複数の言語が入っているようなものも両者にまたがるような変更は自由度が高すぎてLLMと対話的に自分の望むものに近づけていく作業が難しい。
一つのコード、一つの言語で、一つの単位が提供されている方がLLMとやりとりして開発していくのに向いている。
逆に言えば、複数のファイル、言語にまたがるような特定の作業があって、それをLLMで色々出来るようにしたい、 という場合は、それらのまたがった何かを単一の言語、ソースに統合してみせるようなプログラム言語を開発すると、 LLMのでの生成と相性の良い環境を提供できる可能性がある。
後にも述べるように、コードをどれだけ読まなくて済ませられるのか、というのが、LLMを使うメリットと大きく関連する。 だからコードを読まなくてもその生成したコードは「これをする機能」と言えるようなものになっている方が良い。 「AもBもCも出来る環境でAをするコードをLLMに生成させました」では、本当にBやCをしていないかはコードを読まないと確認できない。 「フィルターを記述する言語環境でフィルタのコードを生成させました」なら、コードを読まなくてもこれがフィルターであるのは明らかだ。 少なくともフィルター以外には害がない事も保証出来る。
段階的なコードレビューが可能な方が良い
前回までの記事でも見てきたように、LLMが生成したコードをレビューするのは凄く大変な割に、ご利益は少なく、 LLMに生成させるメリットの多くが失われてしまう。 だから理想的には生成したコードを一才見ずに採用出来るのが望ましい。 どれだけ生成したコードを読まないで済ませられるか、というのが、LLM向けの言語設計がどれだけ優れているか、という事の一つの指標になるんじゃないか。
現実としては、完全に読まないで済ます、というのは、現時点では難しいと思う。 そこで現時点の設計指針としては、全部読むか全く読まないかの間の、 ある程度のチェックは簡単に出来る、というような、「ある程度」を段階的に設計する事なのではないか、と思う。
例えばMFGに関して言えば、DRAMのメモリサイズはテンソルの @bounds 属性で指定する事になる。
この値だけをチェックすれば、それ以外のコードを一切読まなくても確保されるDRAMのサイズは確定させられる。
実行のループに関してはrsumやreduceなどの一部のループ系関数の範囲に限定される。 これはネストされうるので、原理的にはかなり複雑で理解の難しいコードも書く事は出来るが、 現実的に使われる多くのケースではネストはせいぜい2段で、範囲を理解するのも容易なものが多い。 この範囲だけを理解すれば、中のコードを読まなくても個々のカーネルの実行コストの大まかな概算は可能となる。 これはwhileとbreakなどのループではより難しくなる。MFGではそうしたループが無いのは意図した設計である。
このように、「コードのここの部分だけを読めばこれは保証出来て、それはコード全体を読むよりはずっと楽」という切り口が用意できれば、 LLMにコード生成させるメリットを残しつつ、どんなコードが中で動いているのか全く把握していないものを使う、 という状態とは違う事は出来るのではないか。
MFGは多くのフィルタでは、「中の挙動は良くわからないがどういうループでどういうテンソルを処理するかは簡単に理解出来る」というコードが多い。 そこまで確認出来れば、表示の詳細の妥当性に関してはわからないにせよそんなに変な事はやってない、という事は確認出来る。
もちろん表示の所でよくわからない暗号的なものを画像に埋め込んだりするリスクはあるけれど、処理するピクセルのパターンが明快な時にそういうのを埋め込むのはかなり難しいので、 完全に何も見ない事に比べればかなり挙動は理解出来ていると言えると思う。
これはLLMを前提とした言語設計という話であれば、言語設計に組み込むべき事になると思う。 MFGで言うならテンソルという概念があってサイズが属性で指定されて、この属性の計算にはさまざまな制約がある事でそこだけを理解する事が出来る、 というような話がそれである。
余談: なぜMFGはそのような言語になっているのか?
MFGはLLMによるコード生成の実用化よりも前にデザインした言語なのに、いくつかの側面ではLLM向きになっている。 これは本来は別の目的のためにそう作ったのが、たまたまLLMの文脈では良い性質になっていた、という事だ。 ではその本来の別の目的はどういうものだったのか?というのを最後に軽く触れておきたい。
プログラムによる安全性の検証のしやすさを目指していた
MFGでは、最終的には、何も知らない相手が作ったフィルタでも安全にロード出来るようにしたい、という目標を持って作っていて、 そのためのセキュリティのために色々考えた仕様、というのが、今回LLMの生成した結果の扱いにも良い形で活きている、という事になると思う。
ただ全く知らない匿名掲示板とかに貼られたフィルタとかでも安全にロード出来る、というのは最終目標であって最初の段階では目指していない。 MFGは、現時点ではある程度確認されたフィルタをロードする、という前提でいる。 公式でストアを運営し、そこには我々が確認したフィルタだけを登録する、という形になっている。 verifyも考えられる限りの全てをverifyしている、という訳では無い。
だが将来的にはよりverifyを増やして安全性を高めていけるようにしたいな、 とは思っていて、言語仕様はそれを踏まえて設計してある。
具体的には、以下のような特徴を持っている。
- DRAMサイズのプログラムによる検証のしやすさ
- ループの検証のしやすさ
- 検証しやすいIR
最後の検証しやすいIRは今回は関係ないので、上の二つについて以下に簡単に説明しておく。
DRAMサイズのプログラムによる検証のしやすさ
DRAMのサイズの検証は、HalideがDRAMのサイズを自動で推測するのを参考にして仕様を考えている。
ただDRAMのサイズの自動推計自体は実装していない。 これは思ったサイズと違った事になる事も多く、 そうした事を確認するのがかえって面倒が多いので、 使ってみると見た目ほど便利じゃない、と思ったから。
MFGではDRAMはテンソルという形でしかアクセス出来ず、 テンソルのサイズはコード内で明示的に指定する必要がある。 しかもそれはアトリビュートという形でカーネルの外で指定していて、検証も簡単に出来るような特殊なシンタックスになっている。 アクセス時も必ずサイズがわかっているので、範囲外アクセスなどは許さないように出来る。
同じ場所にサイズ指定があってそれが属性という特殊な要素なのはプログラム的な検証のしやすさのためだったのだけれど、 LLMの生成したコードを人間がレビューする時にも検証しやすいというメリットがあった。
ループの検証のしやすさ
GPUプログラムではカーネルの実行が一定時間で終わる必要がある。 だが、この停止判定というのは一般には不可能である事も知られている。 実際、MFGでもタイムアウトするようなコードを書く事は出来る。
ただ、簡単なケースでは簡単にループの回数がプログラム的に検証できるようにはなっている。 この辺はrange周りの特別扱いやreduce系列を主体として副作用の無いループが主体な事などがそれにあたるが、 それはプログラム的な検証が簡単なだけじゃなくて、人間にとっても理解しやすくバグりにくいという良い性質があるので、 最近の言語ではよくある特徴でもある。
DRAMのサイズ検証にはループの範囲の解析が必要になる事があるのでその辺も踏まえて検証しやすいようにしてある。
この辺のプログラムからの検証のしやすさが、人間による検証のしやすさという意図しないメリットを生んでいた。