- 約3000バイトのCコードだけでGPT-2推論器を構成し、重みの読み込みからトークン化・Transformer実行・出力変換までを一連の流れで処理する
- 小さなコードサイズを保ちながらも、KVキャッシュ、高速な行列積、オプションのOMP並列化により、GPT-2 Smallの応答を最新のマシンで数秒以内に生成する
- 出力品質は「客観的にはかなり悪い」水準で、UTF-8処理や大型モデル実行時のメモリ要件といった実用上の制約が残っている
- 実装は行列演算、ニューラルネットワーク層、Transformer、Byte Pair Encoding、I/O、重み・BPE読み込みに分かれており、小さな推論器の全体構造を示している
- GPT-2はGPT-4よりはるかに弱い2019年のオープンソースモデルだが、最新の言語モデル実行に必要な中核部品が小さなCコードでも表現可能であることを示している
3000バイトのCで作られたGPT-2実行器
- このプログラムは依存関係のないGPT-2実装で、元のTensorFlowファイルから重み行列とBPEファイルを読み込む
- 入力は簡単な**Byte Pair Encoding(BPE)**エンコーダでトークン化し、出力はBPEデコーダで再び文字列に変換する
- 内部構成は基本的な線形代数パッケージ、行列演算、Transformerアーキテクチャ、推論コードへと続く
- コードはGitHubで公開されている
- GPT-2 Smallは最新のマシンで1つの応答を数秒程度で生成する
- KVキャッシュを実装している
- 効率的な行列積を使用している
- オプションでOMP並列化を有効にできる
実行条件と限界
- この実装でChatGPTのような対話型プログラムを作ることはできるが、出力品質は良くない
- UTF-8文字の処理にはいくつか特有の問題がある
- XLサイズのモデルを長いコンテキスト長で実行すると、約100GB RAMが必要になる場合がある
- ASCII入力とGPT-2 Smallの組み合わせであれば、ほぼどこでも実行可能である
GPT-2とTransformerの動作
- ChatGPTは言語モデルと人間のように対話できるアプリケーションであり、GPT-4はChatGPTを動かす最新モデルとして紹介されている
- このCプログラムは、2019年のモデルであるGPT-2でChatGPTに似た動作を実現する
- GPT-2はTransformer系の機械学習モデルである
- Transformerは固定サイズの単語シーケンスを入力として受け取り、次の単語を予測する
- 同じ手順を繰り返すことで、任意長のシーケンスを生成できる
行列演算とマクロベースの圧縮
- ニューラルネットワークは行列演算で構成されるため、実装は最小限のMatrix構造体から始まる
float* dat
int rows, cols
- 必要な演算は大きく2種類である
- Cマクロで繰り返しのループ構造を減らし、特定の演算子だけを差し替えて複数の関数を生成する
- Cの
#defineは単純置換に近いため、通常の演算子だけでなくセミコロンを含む式もマクロ引数に渡してコードサイズを縮小できる
高速な行列積
- 基本の行列積は、3重ループを使う**単純なO(n³)**実装から始まる
- キャッシュとメモリアクセス特性を考慮し、同じメモリを繰り返し読み書きするようにループを変更する
- 高速実装では
jとkを4ずつ増やし、内部でk2、j2ループを使う
- 推論段階では、すでに計算した一部の結果を再利用するため、行列Aの一部だけをBと掛ける方式が追加されている
ニューラルネットワーク層の実装
- Transformerを作るために、いくつかのニューラルネットワーク層を直接実装している
- GELU活性化関数はマクロで実装されている
- causal attentionのために、行列の下三角部分を処理する関数がある
- 未来のトークンを見ず、過去だけを見るようattention行列を制限する
- LayerNormは各層の平均と分散を正規化する
- Linear関数は行列積の後にbiasをタイル状に加える
Transformer本体
- Transformer実装では、各層ごとに次の流れを繰り返す
- LayerNormとLinearを経てquery, key, valueを一度に計算する
- headごとにqkvを分割する
- queryとkeyの積を計算し、causal attention処理を適用する
- softmax結果をvalue行列と掛ける
- 結果を集めてresidual connectionを適用する
- GELUとLinearを経て、再びresidual connectionを適用する
- 最後に最終LayerNormを通した後、最後のトークン位置の出力と埋め込み重みを掛けて次トークン候補を計算する
KVキャッシュ方式
- Transformer推論では、1トークンを生成した後に次のトークンを作る際、関数全体を再計算する必要はない
- N番目のトークンまで計算した結果の大半を再利用すれば、N+1番目のトークン生成には一部の追加作業だけで済む
- 実装では、すべての割り当てを同じメモリブロック内で順次行う
- 各行列積が常に同じメモリを使うようにして、次の反復でメモリを0初期化せず前回の結果を保持する
- 新しい反復ではN+1番目の行だけを計算する
Byte Pair Encodingの実装
- 言語モデルは固定サイズの入力を必要とするため、無限に多い単語をそのまま単語単位で扱うのは難しい
- 文字単位モデルでは、すべての単語の意味を最初から学習しなければならず、平均単語長の分だけ有効なコンテキストサイズが縮む問題がある
- GPT-2のようなモデルは、単語の断片でトークンを作るBPEを使う
- よく使われる単語は1つのトークンになりうる
- まれな単語はより小さな断片に分割される
- 例として
nicholasはnich、o、lasのように分かれることがある
- 一般的なBPEアルゴリズムは、隣接するトークン対を繰り返しマージする
- このC実装はコードサイズを減らすため、線形時間アルゴリズムの代わりに、最悪で指数時間がかかりうる再帰方式を使っている
- 現在の単語のprefixと一致するvocabulary項目を探す
- 残りの文字列を再帰的にトークン化する
- 長さとvocabularyインデックスを基準に最良のトークン化を選ぶ
重みの読み込み
- ニューラルネットワークの重みはディスクから読み込む必要があり、ファイルは32ビットfloatのフラットなバイナリ直列化形式である
- GPT-2の各モデルサイズは同じアーキテクチャを使い、重みも同じ順序で保存されているため、正しい形状の行列を順番に読めばよい
- レイヤーの保存順序は期待と異なる
- レイヤー0、1の次に10が来る
- 名前がlexicographic orderでソートされているためである
- 文字列ソートでは
10が2より前に来る
- 実装では、この順序を実際のレイヤー順に直すためにpermutationコードを使用する
BPE vocabularyの読み込み
- BPEを実行するには、まずvocabularyファイルをディスクから読み込む必要がある
- 元のファイルはPythonで読むための形式であり、小さなCコードで簡単にパースできるような形式ではない
- ファイルは単語リストではなくBPEマージ一覧である
- たとえば
Helloというトークンが直接保存されるのではなく、Hとelloをマージすべきだという形で保存される
- ファイルはUTF-8に似ているが、正確には同じではないエンコーディングを使っている
- 表示可能なASCII文字はそのまま保存される
- 0〜31の表示不可能な文字は
188 + 文字値でエンコードされる
- たとえば空白は
Ġトークンとしてエンコードされる
- ディスク上の
ĠはUTF-8では0xc4 0xa0なので、これを再び空白に戻すための別処理が必要になる
小さなコードが示すこと
- 数十年にわたる機械学習の発展を数千バイトのコードに圧縮できる
- 実際のモデル重みを除けば、最新のニューラルネットワークを実行するために必要な要素はほとんど欠けていない
- この実装は主に遊び心で作られたものだが、ニューラルネットワークが実際には単純な構成要素で動かせることを示す一例である
1件のコメント
Hacker News のコメント
コードを実際に動かしてはいないが、サイズが小さい点が印象的
初期の ELIZA プログラムのほうが大きかったことを考えると、この4年ほどでこういうものをバイト単位に詰め込めるようになったということ
どこに魔法が隠れているのか知っている人がいれば説明してほしい。GELU 関数なのか、それとも Bash スクリプトでダウンロードするモデルなのか気になる
Who are you?にはI am Alice.と答え、コンピュータや機能について尋ねるとI am a computer model trained by OpenAI. How can I help you?を繰り返す足し算の説明を求めると掛け算の説明を返し、
2+2やSum 2+2はそのまま復唱する程度GPT-2 が初めて出たときに触っていた記憶がある
友人とのチャットログをエクスポートして GPT-2 をファインチューニングし、2人の会話をまねさせたところ、ものすごく笑える一方で、ときどきぞっとするほど正確だった
GPT-2 から GPT-3 への飛躍的な変化が何によるものだったのか気になる。より大きなモデルなのか、より多くのデータなのか、それとも両方なのかは分からない
RLHF が大きな違いを生んだことは分かるが、ベースの GPT-3 モデルも、例を十分に与えればテキスト補完だけでかなり有用だった
よく分からないが、GPT-2 が書いてくれたお気に入りの童話がいくつかある
https://deepdreams.stavros.io/episodes/the-princess-the-fair...
このページの GPT-2 で作ったものなのか気になる
「大半は遊びで作ったものだが、ニューラルネットワークが実際にはどれほど単純かを示す良い例だ」というくだりが面白い
しっ、誰にも言わないでおこう。人工知能は金もうけのために使う黒魔術だ
GPT-2 は命令チューニングされていて、実際のチャットに使えるのか?
そうでないなら、これを ChatGPT クローンと呼ぶのはかなり無理があると感じる
実質的には使えず、名前を借りている以外にはほとんど関係がない。それでもコンパイルできて実行できるプログラムではある
作った本人もまともに動かないと認めているプロジェクトの性能を高く評価する反応を見ると、結局は流行語で注目を集めることが核心のように見える
「まともなマクロがある言語を見ているか。Lisp が常に C より優れているわけではない!」という文句は、今回は許容できる。上に向けた冗談だから
コードのリンクを見逃したなら本文中に埋もれている: https://github.com/carlini/c-chat-gpt-2
古典的な人工知能チャットボットで、もっと良いものも見たことがある
https://www.cs.cmu.edu/afs/cs/project/ai-repository/ai/areas...
Splotch は少し直せば現代の Unix 系でもうまくコンパイルできる
ローカルで動かして、この GPT-2 がどんな出力をするのか確認した人がいるのか気になる
それでもかなり興味深く、自分で中をのぞいて調整してみたい。しばらくローカルで GPT-2 をいじってみたいと思っていた
コード内で temperature と seed は直接確認できず、主にどうして難読化したのかを見ようとしていたところだった
難読化を解除してもコードはものすごく長くはならなさそうで、だいたい1万文字くらいなら、画面で見るだけでも十分印象的だと思う
最近は gptscript を使えば、自分だけの ChatGPT をすぐ実装できる
https://github.com/gptscript-ai/gptscript
GELU は本当に魔法のようだ:
UNARY(GELU, b / 2 * (1 + tanh(.7978845 * (b + .044715 * b * b * b))))定義は
GELU(x) := x * Φ(x)で、ここで Φ(x) はガウス分布の累積分布関数