# 策展 · X (Twitter) 🔥🔥

> 作者：Akshay 🚀 (@akshay_pachaar) · 平台：X (Twitter) · 日期：2026-05-04

> 原始來源：https://x.com/akshay_pachaar/status/2050941458614751327

## 中文摘要

# LLM 推論是如何運作的

這是一篇關於從你輸入 Prompt 到模型串流輸出回應之間，到底發生了什麼事的基礎原理導覽：包含 tokenization、embeddings、attention、Prefill/Decode 分割、KV caching 以及量化 (quantization)。

---

你輸入了一段 Prompt。幾百毫秒後，文字開始一個接一個地串流回傳給你。這看起來很簡單，但其實不然。

在你按下按鍵到第一個 token 出現之間所發生的事，是現代電腦運算中最精心設計的管線之一。而最奇特的部分在於：模型為了回答你，會在同一個 GPU 上、同一個請求中，執行兩項截然不同且瓶頸各異的工作。

一旦你看懂了這些，你就再也不會用同樣的眼光看待 `generate()` 呼叫了。

# 心智模型

LLM 是一個預測下一個 token 的神經網路。僅僅是一個 token。接著它會取得該 token，將其接在你 Prompt 的末尾，然後預測下一個。接著不斷重複。

就是這樣。這就是整個迴圈。

有趣的問題是：它是如何預測下一個 token 的？以及為什麼第二個 token 的產出速度比第一個快得多？

# 第一步：你的文字變成數字

神經網路不讀英文，它們讀的是向量 (vectors)。所以你的 Prompt 經歷的第一件事是 tokenization，它會將你的文字切成碎片，並為每個碎片分配一個整數 ID。

大多數現代 LLM 使用一種稱為 Byte Pair Encoding 的方案。其概念是：從原始字元開始，不斷合併最常見的相鄰配對，直到擁有約 50,000 個區塊的詞彙表。像 "the" 這種常見詞彙會得到一個 token。像 "unhappiness" 這種罕見詞彙則會被拆解成 "un" + "happi" + "ness" 等碎片。

```python
prompt = "How does inference work?"
ids = tokenizer.encode(prompt)
# ids -> [2437, 1374, 32278, 670, 30]
```

這個步驟的重要性超乎人們想像。那些在 tokenizer 訓練資料中表現不佳的語言會被切成更多碎片，這意味著更多的 token，也意味著同樣的句子需要更高的成本與更慢的回應速度。

# 第二步：每個 token 變成一個向量

每個整數 ID 都會從一個稱為 embedding table 的巨大矩陣中進行查找。如果你的模型詞彙表大小為 50K，隱藏維度 (hidden dimension) 為 4,096，那麼該表格的形狀就是 [50000, 4096]。選取一行，就能得到一個向量。

```python
# embedding_table 的形狀為 [vocab_size, hidden_dim]
vectors = embedding_table[ids]   # 形狀: [num_tokens, 4096]
```

這些向量並非隨機的。在訓練過程中，模型會調整它們，使得語意相似的 token 在這個 4,096 維的空間中彼此靠近。"king" 和 "queen" 是鄰居。"python" 和 "snake" 在一個軸上是鄰居，而 "python" 和 "javascript" 在另一個軸上則是鄰居。

Embedding 層也是注入位置資訊的地方，因為 attention 本身並不知道哪個 token 先出現。現代模型使用像 RoPE 這樣的方案，根據 token 在序列中的位置來旋轉向量。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1777858687998-iaHHZcUu3bQAAMf9Bjpg.jpg)

# 第三步：Attention 層

現在真正的重頭戲開始了。你的向量序列會被送入一疊 Transformer 層中，通常有 32 層或更多，一層接著一層。每一層大致做同樣的事情：

1. 使用 self-attention 在 token 之間混合資訊。

1. 使用前饋網路 (feed-forward network) 在每個 token 內部混合資訊。

Self-attention 是最值得深入理解的部分。對於每個 token，該層會透過與三個已學習的權重矩陣相乘，產生三個新的向量：

```python
# x 是該層的輸入，形狀為 [num_tokens, hidden_dim]
Q = x @ Wq # queries
K = x @ Wk # keys
V = x @ Wv # values
```

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1777858688000-diaHHZaObaMAAU8qvjpg.jpg)

現在你擁有了每個 token 的三種視角。訣竅在於：每個 token 使用它的 query 去查看其他所有 token 的 key，匹配的強度決定了要混合多少其他 token 的 value。

```python
# scores: 每個 token 對其他所有 token 的關注程度
raw     = Q @ K.T
scaled  = raw / sqrt(hidden_dim) # 保持 softmax 穩定
weights = softmax(scaled) # 每行一個 token，總和為 1
attention_output  = weights @ V
```

以下是上述過程的視覺化表示：

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1777858688008-iaHHZbZaVbkAA79UOjpg.jpg)

這就是魔法所在。一個 token 透過環顧四周並提取它認為有用的資訊，來決定它需要什麼 context。堆疊 32 層這樣的結構，你就能得到一個可以追蹤數千個 token 之間參照關係的模型。

在 attention 之後，每個 token 的向量會通過一個小型兩層前饋網路，這執行了模型大部分實際的「知識」。Attention 負責移動資訊，而前饋網路負責處理資訊。

# 第四步：預測下一個 token

在最後一層之後，模型會取得最後一個位置的向量，將其投影回詞彙表大小，並應用 softmax 來獲得每個可能下一個 token 的機率。從該分佈中進行採樣，你就得到了第一個生成的 token。

現在我們進入有趣的部分。

# 兩個沒人告訴你的階段

生成 200 個 token 的回應並不是一項任務。在底層看來，這是兩項完全不同的任務。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1777858688002-iaHHZRSvhbAAAcZ6xjpg.jpg)

## 階段 1：Prefill

當你提交 Prompt 時，模型必須在生成任何內容之前處理你所有的輸入 token。好消息是：它可以並行處理。每個 token 的 Q、K 和 V 都是同時計算的。Attention 運作起來就像一個巨大的矩陣乘法。

GPU 非常喜歡這樣。矩陣乘法正是它們被設計出來的目的。這裡的瓶頸在於原始算術吞吐量：GPU 處於高負載狀態，以矽晶片允許的最快速度進行數學運算。

此階段的指標是 Time to First Token (TTFT)。這是第一個字出現在螢幕前之前的閒置時間。

```python
# Prefill: 一次處理整個 Prompt
hidden = embed(prompt_tokens) + positions
for layer in model.layers:
    Q, K, V = project(hidden)             # 同時處理所有 token
    hidden  = attention(Q, K, V) + hidden
    hidden  = feedforward(hidden) + hidden
    cache_kv(layer, K, V)                 # 儲存以供後續使用
first_token = sample(project_to_vocab(hidden[-1]))
```

## 階段 2：Decode

一旦第一個 token 出來，模型就會切換模式。為了生成第 51 個 token，它只需要計算該單一 token 的 Q、K 和 V。之前的 50 個 token 呢？它們的 K 和 V 向量並沒有改變。重新計算它們將是浪費。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1777858688062-iaHHZZ9M3akAAzVBLjpg.jpg)

因此，模型會進行迴圈，一次一個 token：

```python
# Decode: 每次迭代一個 token
token = first_token
steps = 0
while token != STOP and steps < MAX_STEPS:
    x = embed(token) + position(steps)
    for layer in model.layers:
        q, k, v = project(x)
        K_all, V_all = caches[layer].append(k, v) # 快取歷史 + 新增
        x = layer.forward(q, K_all, V_all, x)  # attention + FFN, residuals
    token = sample(project_to_vocab(x))
    steps += 1
    yield token
```

注意發生了什麼變化。你不再是將一個 query 矩陣與一個 key 矩陣相乘，而是將單一 query 向量與一個 key 矩陣相乘。算術運算量非常小。

但 GPU 仍然必須從記憶體載入每個權重矩陣，以及從記憶體載入每個快取的 K 和 V 來進行那小小的計算。瓶頸瞬間翻轉。晶片有足夠的運算空間，卻只是坐在那裡等待記憶體傳遞下一批資料。

這就是為什麼 Decode 是記憶體受限 (memory-bound)，而 Prefill 是運算受限 (compute-bound) 的原因。同樣的模型、同樣的硬體，卻有完全不同的效能特性。

這裡的指標是 Inter-Token Latency (ITL)：連續輸出的 token 之間的間隔。低 ITL 才會讓模型感覺反應迅速。

# KV cache：讓這一切可行的優化

上面那行 `append_to_cache` 承擔了所有繁重的工作。沒有它，生成 1,000 個 token 的回應意味著在每一步都要為整個不斷增長的序列重新計算 attention。這會導致二次方複雜度，並且慢得令人痛苦。

有了它，你只需儲存一次 K 和 V 矩陣，然後永遠重複使用它們。以下是大致的運作方式：

```python
# 每個 transformer 層一個 KVCache 
class KVCache:
    def __init__(self):
        self.K = None # 到目前為止看到的所有 keys，形狀 [tokens, dim]
        self.V = None # 到目前為止看到的所有 values，形狀 [tokens, dim]

    def append(self, k_new, v_new):
        if self.K is None:
            self.K, self.V = k_new, v_new # 第一個 token
        else:
            self.K = concat([self.K, k_new], axis=token_axis)
            self.V = concat([self.V, v_new], axis=token_axis)
        return self.K, self.V # 到目前為止的完整歷史
```

速度提升非常巨大。對於長生成任務，速度提升可達 5 倍甚至更多。但這是有代價的：快取存在 GPU 記憶體中，並且隨著每個 token 的增加而增長。每一層都保留自己的 K 和 V 張量。對於一個 13B 的模型，每個 token 大約需要 1 MB。一個 4K token 的 context 僅快取就會消耗 4 GB 的 VRAM。

這就是為什麼長 context 感覺緩慢且昂貴的原因。這不是模型耗盡了腦力，而是快取耗盡了空間。

解決方案很有創意：將快取量化為 INT8 或 INT4、丟棄滑動視窗之外的 token、在 attention heads 之間共享 K 和 V (grouped-query attention)，或者像作業系統分頁記憶體那樣對快取進行分頁 (PagedAttention，這是 vLLM 背後的訣竅)。

# 前沿研究：縮減快取本身

量化和分頁將快取視為固定成本。DeepSeek 在 2025 年底預覽的 V4 系列採取了更激進的路線：重新設計 attention，使快取從一開始就很小。

他們的混合方案結合了兩種壓縮 attention 變體，一種稀疏，一種密集，兩者都在經過高度壓縮的 KV 串流上運作。在一百萬個 token 的 context 下，V4-Pro 的快取大小僅為前代產品的 10% 左右，每個 token 的運算量僅為 27%。

重點不在於特定的架構，而在於 KV cache 已經成為該領域目前圍繞其優化模型的瓶頸。當 attention 本身為了最小化快取而被重新設計時，你就知道限制因素已經轉移了。

如果你想了解長 context 推論的發展方向，這非常值得一讀。完整技術報告在這裡：DeepSeek-V4 paper

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1777858688014-iaHHYzpXlbQAAIhycjpg.jpg)

# 量化：以位元換取速度

訓練需要精度，但推論不需要。

大多數生產部署在 FP16 或 BF16 下執行，而不是 FP32，這在 Tensor Cores 上將記憶體需求減半，並使吞吐量大致翻倍。更激進的設定會走得更遠，將權重量化為 INT8 甚至 INT4。

數學很簡單。一個 7B 參數的模型需要：

- FP32 下 28 GB

- FP16 下 14 GB

- INT8 下 7 GB

- INT4 下 3.5 GB

最後這個數字就是為什麼你可以在筆電 GPU 上執行 7B 模型的原因。像 GPTQ 和 AWQ 這樣的方法會選擇逐通道 (per-channel) 的縮放因子，使有損壓縮對品質的損害降到最低。如果做得好，INT4 在大多數基準測試中與原始模型的差距可以控制在一個百分點以內。

# 總結

以下是一個 Prompt 從頭到尾的完整旅程：

1. Tokenize。文字變成整數 ID。

1. Embed。ID 變成向量。位置資訊被整合進去。

1. Prefill。每一層並行處理每個輸入 token。運算受限。KV cache 被填入。第一個輸出 token 出現。

1. Decode 迴圈。對於每個新 token：為新 token 投影 Q，在快取的 K 和 V 上進行 attention，執行前饋網路，採樣。將新的 K 和 V 加入快取。記憶體受限。

1. Detokenize。Token ID 被映射回字元並串流到你的螢幕上。

現代服務框架如 vLLM、TensorRT-LLM 和 Text Generation Inference，透過連續批次處理 (來自多個使用者的 token 在同一個 GPU 步驟中交錯)、推測解碼 (speculative decoding，由小模型起草 token，大模型驗證) 以及巧妙的記憶體管理來封裝這個迴圈。這就是單個 GPU 如何服務數十個並發使用者的原因。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1777858688004-iaHHZdvjDbgAIO7ewjpg.jpg)

# 這應該如何改變你的思維

一旦理解了這些，有幾個實用的心得：

- 長 Prompt 在 TTFT 上很昂貴，長輸出在 ITL 上很昂貴。它們會造成不同的壓力。針對你使用者實際感受到的部分進行優化。

- Context 長度並非免費。將其加倍不僅僅是運算量加倍；它會使 KV cache 膨脹並耗盡你的批次大小 (batch size)。

- 量化是你手中槓桿效應最高的工具。從 FP16 轉向 INT8 通常能將延遲減半，且品質損失微乎其微。

- GPU 使用率可能會誤導人。一個在 Prefill 期間將 GPU 佔滿的模型，在 Decode 期間可能只有 30%。解決方法不是增加運算能力；而是更快的記憶體或更小的快取。

Transformer 架構獲得了所有關注，但推論效能的成敗取決於那些無聊的東西。記憶體佈局、快取管理、位元寬度。藝術在於從你擁有的硬體中榨出最大效能。

現在，當有人告訴你他們的模型很慢時，你就會知道該先問哪個問題：是啟動慢，還是串流慢？

---

如果你喜歡這篇文章，請在留言中告訴我。

這會給我一個訊號，讓我知道應該創作更多類似的內容。

感謝閱讀！

Cheers! :)

## 標籤

LLM, 教學資源, LLM
