第2章 LLM 推論の最小概念
この章では、llama.cpp のソースを読み進めるために最低限必要な LLM の概念を整理します。
扱うのは、トークン、埋め込み、Attention、ロジットとサンプリング、KV キャッシュ、そして prefill と decode という言葉です。
LLM の理論を網羅することが目的ではありません。 あくまで、これから読むソースコードの「地図の凡例」をそろえることが目的です。 いまは厳密さよりも、それぞれの言葉が推論のどの段階に対応するのかをつかむことを優先してください。 ここで一度に全部を覚える必要はありません。後の章で実際のコードと結びつけながら、繰り返し戻ってくる言葉ばかりです。
2.1 言語モデルは「次のトークン」を予測している
LLM が推論時にやっていることは単純です。 これまでの文章を受け取り、「次に来そうな単語の候補それぞれに、どれくらい起こりやすいか」という確率を計算する、ただこれだけです。
たとえば「今日の天気は」という入力に対して、モデルは「晴れ」「雨」「曇り」……といった候補に確率を割り当てます。 その中からひとつを選んで末尾に付け足し、こんどは「今日の天気は晴れ」を入力として、また次の単語の確率を計算する。 この「予測して、選んで、付け足す」を繰り返すことで、文章が一語ずつ伸びていきます。 ひとつ前の出力を次の入力に回すこの仕組みを、 自己回帰(autoregressive) と呼びます。
図 2-1 自己回帰による1トークン生成ループ
モデルは一度に文章全体を吐き出しているのではなく、 1 トークンずつ生成しています 。
llama-cli の画面で文字が少しずつ流れていくのは、この自己回帰のループがそのまま見えているからです。
推論とは「次の 1 トークンを予測するループ」です。 以降の節では、このループの中身を分解していきます。
2.2 トークン:テキストを区切る単位
前の節で「単語」と書きましたが、より正確には、モデルが扱う単位は トークン(token) です。
トークンは単語そのものとは限りません。
英語なら ing のような接尾辞が 1 トークンになったり、日本語なら数文字のまとまりが 1 トークンになったりします。
モデルは文字列を直接は扱えません。 そこで、入力テキストはまずトークンの列に分解されます。この処理を トークン化(tokenization) と呼びます。 分解の規則と、扱えるトークンの一覧を 語彙(vocabulary) と呼びます。
プログラマ向けに言い換えると、語彙は「トークンの配列」、そして各トークンには配列の添字にあたる整数の トークン ID が割り当てられている、というイメージが近いです。
llama.cpp でも、トークンは内部的にはただの整数として流れていきます。
テキストとトークン ID の相互変換はトークナイザーが担当し、その実体は第 4 部で詳しく読み解きます。
注釈
語彙やトークナイザーの設定は、モデルファイル(GGUF 形式)の中にモデル本体と一緒に格納されています。 つまり「どう区切るか」はモデルごとに決まっています。 GGUF の中身については第 4 部で扱います。ここでは「テキストは整数の列に変換されてからモデルに入る」とだけ押さえてください。
2.3 埋め込み:トークンを意味のベクトルにする
トークン ID は単なる通し番号で、それ自体には意味がありません。 ID が 100 番のトークンと 101 番のトークンが、意味の上で隣り合っているわけではないからです。
そこでモデルは、各トークン ID を 埋め込みベクトル(embedding) と呼ばれる数値の並びに変換します。 これは、トークン ID をキーにして大きな表を引く操作だと考えると分かりやすいです。 表の各行が 1 つのトークンに対応し、その行に「そのトークンの意味を表す数百〜数千個の数値」が並んでいます。
トークン ID 1234 → [0.02, -0.51, 0.88, ... , 0.13] ← 埋め込みベクトル(長さ = 埋め込み次元)
この変換を経て、トークンの列は「ベクトルの列」になります。 以降、モデル内部の計算はすべてこのベクトルに対して行われます。 埋め込みは、記号(整数)を計算可能な数値に翻訳する入口です。
2.4 Transformer の中で起きること(最小限)
埋め込みベクトルの列を受け取って、次トークンの予測材料を作り出すのがモデル本体です。 現在の主流である Transformer というアーキテクチャの内部は、本書全体を通じて少しずつ読み解いていきます。 ここでは、ソースを追ううえで欠かせない 1 つの考え方だけを取り上げます。
Attention:トークンが互いを参照する
文章を理解するには、離れた位置にある単語どうしの関係をとらえる必要があります。 「猫が水を飲んだ。それは喉が渇いていたからだ」の「それ」が何を指すかは、前の文を見なければ分かりません。
この「どのトークンが、どのトークンを、どれくらい参照するか」を計算する仕組みが Attention(注意機構) です。 各トークンは、自分より前にあるすべてのトークンを見渡し、関係の深いものほど強く参照して、自分の表現を更新します。
図 2-2 Attentionで過去トークンを参照
図のように、次のトークンを予測する時点で、モデルは過去のすべてのトークンを参照できます。 この「過去を振り返る」性質が、後で出てくる KV キャッシュの話につながります。
Transformer は、この Attention と、各トークンを個別に変換する処理(フィードフォワード)を 1 つの 層(layer) にまとめ、その層を何十回も積み重ねた構造をしています。 層を通るたびにトークンの表現は文脈を取り込んで豊かになり、最後の層を抜けたときには「次に何が来るか」を予測できる状態になっています。
いまは Attention の数式を追う必要はありません。 「各トークンが過去のトークンを参照しながら、層を重ねて文脈を取り込んでいく」 、この一文だけ持っていれば十分です。 実際の計算がどう組み立てられるかは、第 5 部の計算グラフの章で具体的に見ます。
2.5 ロジットとサンプリング:次のトークンを選ぶ
最後の層を抜けたあと、モデルは語彙に含まれるすべてのトークンに対して 1 つずつスコアを出力します。 このスコアの並びを ロジット(logits) と呼びます。 長さは語彙のサイズに等しく、たとえば語彙が 3 万トークンなら、3 万個の数値が並びます。
ロジットはそのままでは確率ではないので、 softmax という変換を通して「合計が 1 になる確率分布」に直します。 そして、その分布から実際に 1 つのトークンを選ぶ処理を サンプリング(sampling) と呼びます。
図 2-3 ロジットからトークン選択までの流れ
選び方にはいくつかの方針があります。
最も確率の高いトークンを常に選ぶ(決定的で、毎回同じ出力になる)
確率に応じてランダムに選ぶ(多様性が出る。温度、top-k、top-p などで調整する)
同じモデルでも出力の雰囲気が変わるのは、多くがこのサンプリングの設定によるものです。 サンプリングの各手法と、それらを組み合わせる仕組みは第 6 部で詳しく扱います。 ここでは「モデルは確率を出すところまでが仕事で、最終的にどのトークンにするかはサンプリングが決める」という役割分担を押さえてください。
2.6 KV キャッシュ:生成を速くする仕組み
ここで、自己回帰ループに戻ります。 1 トークン生成するたびに、過去のすべてのトークンを参照すると説明しました。 これを素直に実装すると、無駄が生じます。 2 トークン目を作るときも、3 トークン目を作るときも、毎回「最初のトークンから全部」を計算し直すことになるからです。
ところが、Attention の計算過程で各トークンから作られる中間データ(慣習的に「キー」と「バリュー」と呼ばれます)は、一度計算すれば後のステップでも変わりません。 そこで、これらを保存しておいて再利用すれば、新しいトークン 1 つ分だけを計算すれば済みます。 この保存領域が KV キャッシュ(KV cache) です。
図 2-4 KVキャッシュによる計算の再利用
KV キャッシュは生成を大きく速くする一方で、保存するトークン数(=コンテキスト長)に比例してメモリーを消費します。 コンテキストを長くするとメモリーが急に増えるのは、このためです。 KV キャッシュの構造とメモリー管理は、第 5 部で読み解きます。
2.7 prefill と decode:2 つのフェーズ
最後に、推論には性質の異なる 2 つのフェーズがあることを押さえておきます。
ひとつ目は、ユーザーが入力したプロンプトを処理するフェーズです。 プロンプトのトークンはすでに全部そろっているので、まとめて一気にモデルに通せます。 このフェーズを prefill と呼びます。
ふたつ目は、そこから 1 トークンずつ生成していくフェーズです。 こちらは前の出力が決まらないと次に進めないので、1 トークンずつ順番に処理するしかありません。 このフェーズを decode (生成)と呼びます。
図 2-5 prefillとdecodeの2フェーズ
この 2 つは「まとめて処理できるか、1 つずつしか進めないか」という点で計算の効率が大きく異なり、性能を考えるうえで重要な区別になります。
llama.cpp では、どちらのフェーズも同じ入口の関数を通して処理されます。
その入口がどこで、内部でどうフェーズが扱われるのかは、第 5 部のコンテキストとデコードの章で読み解きます。
2.8 この章の言葉と本書の地図
この章で導入した言葉は、いずれも本書のどこかで実際のソースコードと結びつけて再登場します。 最後に、用語と「どの段階の話か」「本書のどこで扱うか」を一覧にまとめておきます。
用語 |
ひとことで言うと |
主に扱う部 |
|---|---|---|
トークン / 語彙 |
テキストを区切る単位と、その一覧 |
第 4 部 |
埋め込み |
トークン ID を意味のベクトルに変換する入口 |
第 5 部 |
Attention |
各トークンが過去のトークンを参照する仕組み |
第 5 部 |
ロジット |
語彙の全トークンに対する次トークンらしさのスコア |
第 5 部、第 6 部 |
サンプリング |
ロジットから実際に 1 つのトークンを選ぶ処理 |
第 6 部 |
KV キャッシュ |
過去の計算を再利用して生成を速くする保存領域 |
第 5 部 |
prefill / decode |
プロンプト一括処理と 1 トークンずつの生成 |
第 5 部 |
表 2-1 本章の用語と本書での対応
この章では、推論を「次の 1 トークンを予測する自己回帰ループ」として捉え、その中に現れるトークン、埋め込み、Attention、ロジットとサンプリング、KV キャッシュ、prefill / decode という言葉を整理しました。
これらはいずれも、 「テキスト → トークン → ベクトル → モデル本体 → ロジット → サンプリング → 次のトークン」 という一本の流れの上に並んでいます。
次の第 3 章では、この流れを llama.cpp の実際の構成要素(ggml、モデル、トークナイザー、サンプラーなど)に対応させ、ブロックダイアグラムとデータフロー図で全体像を俯瞰します。
本章でそろえた凡例が、その地図を読むための手がかりになります。