你输入 prompt,几百毫秒后,单词一个接一个流式返回。感觉很简单。其实不是。
从你的按键到第一个 token 之间发生的事,是现代计算中最精心设计的管道之一。最奇怪的地方?模型在同一次请求中,在同一个 GPU 上,做两个完全不同的工作,有两个完全不同的瓶颈。
LLM 是什么
LLM 是预测下一个 token 的神经网络。只预测一个 token。然后它把那个 token 附加到 prompt 末尾,预测下一个。然后重复。
就这些。这就是整个循环。
有趣的问题是:它如何预测下一个 token,为什么第二个 token 比第一个快得多?
Tokenization:文本变成向量
神经网络不读英文。它们读向量。所以 prompt 的第一件事是 tokenization——把文本切成片段,给每个片段分配整数 ID。
大多数现代 LLM 使用 Byte Pair Encoding(BPE)。思路:从原始字符开始,反复合并最常见的相邻对,直到有约 50,000 个词汇块。常见词如 the 得到一个 token。罕见词如 unhappiness 被分成 un + happi + ness。
prompt = "How does inference work?"
ids = tokenizer.encode(prompt)
# ids -> [2437, 1374, 32278, 670, 30]
这一步比人们意识到的更重要。在 tokenizer 训练数据中表征不足的语言会被切成更多片段,意味着更多 token、更高成本、同样句子的更慢响应。
Embeddings:整数变成向量
每个整数 ID 在一个巨大矩阵(embedding table)中被查找。如果模型有 50K 词汇和 4,096 隐藏维度,表的形状是 [50000, 4096]。选一行,得到一个向量。
vectors = embedding_table[ids] # shape: [num_tokens, 4096]
这些向量不是随机的。训练期间,模型调整它们,让语义相似的 token 在这个 4,096 维空间中靠近。king 和 queen 是邻居。python 和 snake 沿一个轴是邻居,python 和 javascript 沿另一个轴。
Embedding 层也是位置信息注入的地方,因为 attention 本身不知道哪个 token 先来。现代模型使用 RoPE 等方案根据序列中的位置旋转向量。
Transformer 层:Attention + Feed-Forward
向量序列通过一叠 transformer 层,通常 32 层或更多。每层做大致相同的事:
- 用 self-attention 跨 token 混合信息
- 用 feed-forward 网络在每个 token 内混合信息
Self-attention 是值得深入理解的部分。对每个 token,层通过与三个学习的权重矩阵相乘产生三个新向量:
Q = x @ Wq # queries
K = x @ Wk # keys
V = x @ Wv # values
现在每个 token 有三个视图。技巧:每个 token 用它的 query 看其他每个 token 的 key,匹配强度决定混入那个其他 token 的 value 的多少。
raw = Q @ K.T
scaled = raw / sqrt(hidden_dim)
weights = softmax(scaled)
attention_output = weights @ V
这就是魔法。token 通过环顾四周并拉入它认为有用的东西来决定它需要什么上下文。堆叠 32 层,你得到一个可以跟踪数千 token 引用的模型。
Attention 之后,每个 token 的向量通过一个小型两层 feed-forward 网络,做大部分模型的实际"知道"。Attention 移动信息。Feed-forward 网络处理它。
最后一层后,模型取最后位置的向量,投影回词汇大小,应用 softmax 获得每个可能的下一个 token 的概率。从那个分布采样,你得到第一个生成的 token。
Prefill vs. Decode:两个完全不同的任务
生成 200 token 响应不是一个任务。是两个在底层看起来完全不同的任务。
Prefill(预填充)
当你提交 prompt 时,模型必须在生成任何东西之前处理所有输入 token。好消息:可以并行做。每个 token 的 Q、K、V 同时计算。Attention 作为大矩阵乘矩阵运行。
GPU 喜欢这个。矩阵-矩阵乘法是它们设计的用途。这里的瓶颈是原始算术吞吐量:GPU 以高利用率固定,以硅允许的最快速度做数学。
这个阶段的指标是 Time to First Token (TTFT)——第一个词出现在屏幕前的空闲时间。
Decode(解码)
第一个 token 出来后,模型切换模式。要生成第 51 个 token,只需要为那一个 token 计算 Q、K、V。前 50 个 token?它们的 K 和 V 向量没变。重新计算是浪费工作。
所以模型循环,一次一个 token。但 GPU 仍然必须从内存加载每个权重矩阵、每个缓存的 K 和 V 来做那个微小的计算。突然瓶颈翻转。芯片有大量计算余量,只是坐在那里等待内存传递下一块数据。
这就是为什么 decode 是内存密集、prefill 是计算密集。同一个模型,同一个硬件,完全不同的性能特征。
这里的指标是 Inter-Token Latency (ITL)——连续 token 流出之间的间隙。低 ITL 让模型感觉快。
KV Cache:速度的代价
没有 KV cache,生成 1,000 token 响应意味着在每一步为整个增长序列重新计算 attention。二次复杂度,痛苦地慢。
有了它,你保存 K 和 V 矩阵一次并永远重用。加速是巨大的——长生成通常快 5 倍或更多。但有代价:cache 占用 GPU 内存,每个 token 增长。每层保持自己的 K 和 V 张量。对 13B 模型,每个 token 大约 1 MB。4K token 上下文仅在 cache 上就烧掉 4 GB VRAM。
这就是为什么长上下文感觉慢且昂贵。不是模型耗尽脑力。是 cache 耗尽空间。
修复很有创意:量化 cache 到 INT8 或 INT4、丢弃滑动窗口外的 token、跨 attention head 共享 K 和 V(grouped-query attention)、或像操作系统分页内存一样分页 cache(PagedAttention,vLLM 背后的技巧)。
DeepSeek V4 采用更激进路线:重新设计 attention,使 cache 从一开始就很小。100 万 token 上下文,V4-Pro 报告约 10% 的 cache 大小和 27% 的每 token 计算。
教训不是具体架构。是 KV cache 已成为该领域现在围绕优化模型的瓶颈。
Quantization:最高杠杆旋钮
训练需要精度。推理不需要。
大多数生产部署在 FP16 或 BF16 而不是 FP32 运行,内存减半,Tensor Core 上吞吐量大约翻倍。激进设置进一步量化权重到 INT8 甚至 INT4。
7B 参数模型占用:
- FP32: 28 GB
- FP16: 14 GB
- INT8: 7 GB
- INT4: 3.5 GB
最后一个数字是为什么你可以在笔记本 GPU 上运行 7B 模型。像 GPTQ 和 AWQ 的方法选择每通道缩放因子,使有损压缩尽可能少伤害质量。做得好,INT4 在大多数 benchmark 上能在原始的百分之一个点内。
实用要点
- 长 prompt 在 TTFT 昂贵,长输出在 ITL 昂贵。它们压力不同。优化用户实际感受的那个。
- 上下文长度不是免费的。加倍不只是加倍计算;它膨胀 KV cache 并饿死批大小。
- Quantization 是你拥有的最高杠杆旋钮。从 FP16 到 INT8 通常减半延迟,质量损失可忽略。
- GPU 利用率可能误导。prefill 期间钉住 GPU 的模型在 decode 期间可能在 30%。修复不是更多计算;是更快内存或更小 cache。
现在当有人告诉你他们的模型慢,你知道先问哪个问题:是开始慢,还是流式慢?