Transformer 內部解剖
你能在本機跑的模型全是 transformer,而這套架構一張餐巾紙就畫得下:把 token 轉成向量,推過 N 層相同的層、讓 token 彼此觀看,再把最後的向量投影回詞彙表分數。不需要微積分——內積與一個 softmax 就撐起整場秀。這一頁會把整條路慢慢地走一遍。
殘差流
想像一條輸送帶貫穿整個模型。在每一層,子模組讀取輸送帶、把自己的貢獻加回上面——沒有東西被取代,一切都在累加:
x = embed(token_ids) + positions # token 變成向量(維度約 4k)
for layer in range(N): # 8B 模型的 N ≈ 32
x = x + attention(x) # token 之間交換資訊
x = x + ffn(x) # 每個 token 自我轉換
logits = unembed(x[-1]) # 最後位置 → 每個詞彙表條目的分數
p = softmax(logits) # 下一個 token 的機率分布
- 嵌入:每個 token ID 索引一張學出來的向量表。位置資訊會混入其中(現代模型用旋轉位置編碼),所以「狗咬人」與「人咬狗」不同。
- 各層形狀相同、內容不同。淺層處理語法與表面模式;深層承載語意、事實與任務行為。
- 反嵌入:最終向量與每個詞彙表條目比對;softmax 把分數變成機率。取樣挑出一個(取樣)。
用四個 token 看懂自注意力
交換機制。每個 token 的向量衍生出三個向量:查詢(query,我在找什麼)、鍵(key,我對外宣稱什麼)、值(value,被選中時我交出什麼)。句子:"The cat sat on"。"sat" 的位置該拉進什麼?
token: The cat sat on
q·k scores: 1.2 4.1 3.0 0.4 (dot products, illustrative)
softmax → 0.05 0.60 0.31 0.04 (weights sum to 1.0)
output("sat") = 0.05·v(The) + 0.60·v(cat) + 0.31·v(sat) + 0.04·v(on)
"sat" 主要注意到 "cat"——它需要的主詞。每個位置同時執行這件事。一個限制使它成為語言模型:因果遮罩——每個 token 只能看更早的 token,絕不能看之後的,因此一次前向計算就能同時訓練序列中每個位置的下一個 token 預測。
這就是全部的數學工具箱:相似度分數(內積)、正規化成混合比例(softmax)、施加在值向量上。
當作食譜,一個位置上的一個注意力頭:
- 為每個 token 計算查詢、鍵、值向量。
- 把查詢與每個更早 token 的鍵做內積;除以 √(head_dim) 縮小。
- 用 softmax 把分數變成總和為 1 的權重。
- 輸出這些 token 值向量的加權總和。
- 串接所有注意力頭的輸出並投影——這就是該層注意力的貢獻。
多頭:平行的模式
一個頭=一組查詢/鍵/值=一種它能表達的關係(主詞↔動詞、形容詞↔名詞、指代消解)。每一層在低維切片上平行跑多個頭(例如 32 個),再串接結果。更多的頭代表每層有更多種關係——是模式的廣度,不是思想的深度。
- 注意力頭在訓練時就固定了;跨位置的「思考」其實是注意力寫回殘差流的內容。
- 頭維度(例如 128)× 頭數(例如 32)=隱藏維度(4096):頭是同一條輸送帶的平行切片,不是獨立的模型。
FFN:參數的聚集地
每一層的後半是一個雙矩陣網路,獨立作用於每個 token:把向量擴張(約 4×)、非線性、再投影回來。在典型的稠密模型中,約三分之二的參數住在這裡,而不是注意力裡。有用的直覺:鍵值記憶——第一個矩陣存放要偵測的模式,第二個矩陣存放模式命中時要加到輸送帶上的東西。人們所謂「模型的事實」大多存在 FFN 權重裡(見 參數量)。
| 元件 | 參數占比(典型 7–9B 稠密模型) |
|---|---|
| 注意力(Q、K、V、O) | ~10–15% |
| FFN | ~60–70% |
| 嵌入 + 輸出頭 | ~10–25%(隨詞彙表大小增長) |
自己算算參數量
模型卡會公布五個數字;把它們相乘就能得到參數量。以一個真實的 8B 級開源模型為例(32 層、隱藏維度 4096、32 個頭搭配 8 個 KV 頭、FFN 寬度 14336、詞彙表約 128k):
attention/layer: q: 4096×4096 + k,v: 2×(4096×1024) + o: 4096×4096
≈ 42M per layer × 32 layers ≈ 1.3B
FFN/layer: 3 × (4096×14336), gated ≈ 176M × 32 ≈ 5.6B
embeddings: 2 × (128k × 4096), input + output head ≈ 1.1B
total ≈ 8.0B
算出來的結果與模型卡上的「8B」幾乎完全吻合。兩個結論:FFN 主導了參數量(也因此主導記憶體預算);而大型詞彙表在第一層還沒開始跑之前,就要先花掉約十億參數——這是大詞彙表多語模型比同規模稠密兄弟看起來更重的原因之一。
GQA:注意力成本的解方
生成時,每個新 token 都必須讀取之前所有 token 的鍵與值——存放在 KV 快取中。原始的多頭注意力為每個 token 的每個頭各存一份 K 與 V。分組查詢注意力(GQA)讓多個查詢頭共享少數幾個 KV 頭(例如 32 個查詢頭、8 個 KV 頭),以幾乎可忽略的品質代價把快取縮小 4 倍。多查詢注意力(MQA)則只共享單一個 KV 頭。實際上每個現代開源模型都用 GQA;這些節省正是長上下文之所以可用的重大原因(KV 快取)。
上下文長度=注意力跨度
注意力讓每個 token 看得到每個更早的 token,因此預填的計算量隨提示詞長度呈平方增長,而快取隨每個生成的 token 線性增長。「128k 上下文」的模型靠位置技巧與延伸訓練達到這個數字,而視窗遠端的品質通常比起點附近弱。只有在必要時才填滿長視窗。
自己測測看:把一個事實放在視窗開頭、中間塞滿填充文字、再問它。多數模型在視窗的頭尾表現正常、在中間下滑——這就是著名的「迷失在中間」(lost in the middle)模式。把宣稱的上下文當作容量,而不是品質保證。
為什麼這樣就夠了
光從這個結構就能推導出實務事實:記憶體需求=權重+KV 快取(VRAM 用量計算);解碼速度=重新讀取權重的速度(預填與解碼);能力追隨參數量(參數量);上下文成本=注意力跨度。架構很簡單——裡面那數十億個訓練出來的權重,才是沒有人能用手寫出來的部分。