前回の実験では、"Hello" が Tokenizer によって Token ID 9707 になり、その後 token_embd.weight から 2048個の f32 が取り出されるところまで確認した。
"Hello"
↓
Tokenizer
↓
9707
↓
token_embd.weight
↓
GET_ROWS
↓
2048個の f32
実際のログはこうだった。
number of input tokens = 1
9707
common_debug_cb_eval:
embd = (f32)
GET_ROWS(
token_embd.weight{2048, 151936, 1, 1},
inp_tokens{1, 1, 1, 1}
)
= {2048, 1, 1, 1}
値も確認できた。
[
[
[
[ -0.0021, 0.0273, -0.0070, ..., -0.0360, 0.0022, 0.0022 ],
],
]
]
ここで新しい疑問が出てきた。
この2048個の数は、その後どうなるのか?
今回は、もう少しだけ先を追ってみる。
ここまでの流れ
今回追った範囲を、いったん図にして整理する。
┌─────────────────────────────────────┐
│ "Hello" │
└──────────────────┬──────────────────┘
│
│ Tokenizer
▼
┌─────────────────────────────────────┐
│ Token ID = 9707 │
└──────────────────┬──────────────────┘
│
│ index として使用
▼
┌─────────────────────────────────────┐
│ token_embd.weight │
│ │
│ {2048, 151936, 1, 1} │
│ │
│ 151936 : Token ID の数 │
│ 2048 : 1 Token あたりの値の数 │
└──────────────────┬──────────────────┘
│
│ GET_ROWS
▼
┌─────────────────────────────────────┐
│ 2048個の f32 │
│ │
│ [-0.0021, 0.0273, -0.0070, ...] │
│ │
│ llama.cpp 内では inpL という │
│ 変数を通して次の処理へ渡される │
└──────────────────┬──────────────────┘
│
▼
┌─────────────────────────────────────┐
│ モデルの最初の処理層 │
│ │
│ Qwen3MoE には複数の処理層がある │
│ 今回は最初の層だけを見る │
│ │
│ ここに渡されるのは 9707 ではなく │
│ 2048個の f32 │
└──────────────────┬──────────────────┘
│
│ 最初に確認できた処理
▼
┌─────────────────────────────────────┐
│ RMS Norm │
│ │
│ 2048個の数から全体の大きさを計算 │
│ ↓ │
│ scale を1つ計算 │
│ ↓ │
│ 2048個すべてを同じ scale で調整 │
└──────────────────┬──────────────────┘
│
▼
次のモデル計算へ
│
▼
???
inpL とは?
Qwen3MoE のコードを見ると、
ggml_tensor * inpL;
inpL = build_inp_embd(model.tok_embd);
となっている。
inpL は「2048個の f32」の一般的な名称ではない。
この llama.cpp のコード内で使われている変数名だ。
今回確認した流れは、
9707
↓
GET_ROWS
↓
2048個の f32
↓
inpL という変数を通して
↓
次の処理へ
となる。
モデルの最初の処理層へ
Qwen3MoE のコードを追うと、次のループが見つかった。
for (int il = 0; il < n_layer; ++il) {
res->t_layer_inp[il] = inpL;
ggml_tensor * inpSA = inpL;
cur = build_norm(inpL,
model.layers[il].attn_norm, NULL,
LLM_NORM_RMS, il);
il を変えながら、複数の処理層を順番に通っていく。
今回は、その中身をすべて追わない。
注目したいのは、
res->t_layer_inp[il] = inpL;
という部分。
最初の処理層に渡されているのは Token ID の
9707
ではなく、GET_ROWS で取り出した
2048個の f32
だった。
最初に確認できた処理は RMS Norm
続いて、
cur = build_norm(inpL,
model.layers[il].attn_norm, NULL,
LLM_NORM_RMS, il);
が呼ばれている。
さらに追っていくと、
ggml_rms_norm(ctx0, cur, hparams.f_norm_rms_eps);
そして、
GGML_OP_RMS_NORM
に到達した。
名前は RMS Norm(Root Mean Square Normalization)。
CPU 側の実装まで追ってみる。
for (int64_t i00 = 0; i00 < ne00; i00++) {
sum += (ggml_float)(x[i00] * x[i00]);
}
const float mean = sum/ne00;
const float scale = 1.0f/sqrtf(mean + eps);
最後に、
ggml_vec_scale_f32(ne00, y, scale);
が実行されていた。
2048個の数をまとめて使って、何かを計算している。
RMS Norm は何をしている?
2048個では分かりにくいので、3個の数で考えてみる。
仮に、
x = [2, 4, 6]
だったとする。
まず、それぞれを二乗して足す。
2² + 4² + 6²
= 4 + 16 + 36
= 56
次に、数の個数で割る。
56 / 3
≈ 18.67
そして平方根を取る。
√18.67
≈ 4.32
この 4.32 を使って、元の3個の数を調整する。
[2, 4, 6]
↓ ÷ 4.32
[0.46, 0.93, 1.39]
元の比率は、
2 : 4 : 6
= 1 : 2 : 3
調整後も、
0.46 : 0.93 : 1.39
≈ 1 : 2 : 3
となる。
つまり、数同士の相対的な関係をほぼ保ったまま、全体の大きさを調整している。
今回の2048個の f32 に対しても、同じ考え方の処理が行われている。
コードを数式にしてみる
ここまでの処理を数式にするとどうなるのだろう。
今回、
ne00 = 2048
なので、コードの
const float mean = sum/ne00;
const float scale = 1.0f/sqrtf(mean + eps);
は、次の式で表せる。
$$
\frac{1}{
\sqrt{
\frac{x_1^2+x_2^2+\cdots+x_{2048}^2}{2048}
+\epsilon
}
}
$$
一見するとかなり複雑に見える。
しかし、先ほど x = [2, 4, 6] でやったことと同じだった。
それぞれを二乗
↓
全部足す
↓
個数で割る
↓
平方根を取る
↓
逆数を取る
↓
scale
そして、その scale を2048個すべてに掛ける。
$$
y_i = x_i \times \text{scale}
$$
つまり、
[x₁, x₂, ..., x₂₀₄₈]
↓
2048個の数から
1つの scale を計算
↓
[x₁×scale,
x₂×scale,
...
x₂₀₄₈×scale]
となる。
ソースコードで見つけた、
sum += x[i00] * x[i00];
const float mean = sum/ne00;
const float scale = 1.0f/sqrtf(mean + eps);
という処理は、数式にすると、
$$
\frac{1}{
\sqrt{
\frac{x_1^2+x_2^2+\cdots+x_{2048}^2}{2048}
+\epsilon
}
}
$$
だった。
今回分かったこと
今回、Token ID 9707 の先を少しだけ追ってみた。
"Hello"
↓
Tokenizer
↓
9707
↓
index として使用
↓
token_embd.weight
↓
GET_ROWS
↓
2048個の f32
↓
モデルの最初の処理層
↓
RMS Norm
↓
2048個の数を使った数学計算
↓
???
少なくとも今回観察した Qwen3MoE では、Token ID 9707 がそのまま数値として最初のモデル計算に使われているわけではなかった。
9707 はまず token_embd.weight から対応するデータを取り出すための index として使われる。
そして、取り出された 2048個の f32 がモデルの最初の処理層へ渡され、RMS Norm の計算対象になっていた。