从 prompt 到流式返回,中间发生的事情比表面看起来复杂得多。
你输入一段话。几百毫秒之后,模型开始一个词、一个词地往外吐。体验上很简单:发出问题,等回答。
但从你按下回车,到第一个 token 出现在屏幕上,这中间跑过的是现代计算里一条非常精密的流水线。更有意思的是,同一个模型、同一块 GPU、同一次请求里,模型其实在做两种完全不同的工作,而且瓶颈也完全不同。
一旦看懂这条链路,你再看 generate() 就不会觉得它只是一个普通函数调用了。
先建立一个心智模型
LLM 本质上是在预测下一个 token。
只预测一个 token。
预测完以后,它把这个 token 接到 prompt 后面,再预测下一个。然后继续重复。
整个生成循环就这么简单。
真正值得拆的是两个问题:模型怎么预测下一个 token?为什么第一个 token 总是等得更久,而后面的 token 通常出来得更快?
第一步:文本先变成数字
神经网络不直接读英文、中文或者自然语言。它读的是向量。所以 prompt 进入模型前,第一件事是 tokenization:把文本切成一段段 token,再给每个 token 分配一个整数 ID。
现在大多数 LLM 都用 Byte Pair Encoding,也就是 BPE。它的思路是:先从原始字符开始,不断合并最常见的相邻片段,直到得到一个大约 5 万个 chunk 的词表。常见词可能一个 token 就够了,少见词会被切成多个片段。比如 unhappiness 可能会被拆成 un、happi、ness。
python
prompt = "How does inference work?"
ids = tokenizer.encode(prompt)
# ids -> [2437, 1374, 32278, 670, 30]
这一步比很多人想象得重要。某种语言如果在 tokenizer 训练数据里占比不高,同一句话就可能被切成更多 token。token 变多,成本更高,响应也更慢。
第二步:每个 token 变成向量
每个整数 ID 会去一张巨大的 embedding table 里查一行。如果模型词表是 50K,hidden dimension 是 4,096,这张表的形状就是 [50000, 4096]。拿一个 ID 查一行,就得到一个向量。
python
# embedding_table has shape [vocab_size, hidden_dim]
vectors = embedding_table[ids] # shape: [num_tokens, 4096]
这些向量不是随机数。训练过程中,模型会不断调整它们,让语义相近的 token 在这个 4,096 维空间里靠得更近。king 和 queen 会有某种邻近关系。python 和 snake 在一个方向上相近,python 和 javascript 又会在另一个方向上相近。
位置编码也会在这一层或附近进入模型。attention 本身不知道谁在前、谁在后,所以现代模型会用 RoPE 这类方法,根据 token 在序列里的位置旋转向量。

第三步:一层层 attention 开始干活
接下来才是主要计算。整串向量会经过一层又一层 transformer block,常见模型可能有 32 层甚至更多。每一层大致做两件事:
- 用 self-attention 在 token 之间混合信息。
- 用 feed-forward network 在每个 token 内部处理信息。
self-attention 是最值得看懂的部分。对每个 token 来说,这一层会用三组学习出来的权重矩阵,生成三种新向量:Query、Key、Value。
python
# x is the input to this layer, shape [num_tokens, hidden_dim]
Q = x @ Wq # queries
K = x @ Wk # keys
V = x @ Wv # values

现在,每个 token 都有三个视角。关键在这里:一个 token 会拿自己的 query 去和其他 token 的 key 做匹配。匹配强度决定它应该从那个 token 的 value 里拿多少信息。
python
# scores: how much each token attends to every other token
raw = Q @ K.T
scaled = raw / sqrt(hidden_dim) # keeps softmax stable
weights = softmax(scaled) # one row per token, sums to 1
attention_output = weights @ V
上面这件事可以画成这样:

这就是 attention 的核心。一个 token 会环顾上下文,决定自己需要哪些信息,再把这些信息混进自己的表示里。把这样的层叠 32 次,模型就能在几千甚至更多 token 的上下文里追踪引用、关系和约束。
attention 之后,每个 token 的向量还会经过一个小型两层 feed-forward network。attention 负责搬运信息,feed-forward network 负责处理信息。模型里很多"知道什么"的能力,都压在这部分参数里。
第四步:预测下一个 token
最后一层跑完后,模型会拿最后一个位置的向量,把它投影回词表大小,再做 softmax,得到"下一个 token 是词表中每个 token 的概率"。从这个分布里采样,就得到了第一个生成 token。
到这里,真正有意思的部分才开始。
生成不是一个阶段,而是两个阶段
生成一段 200 token 的回答,看起来像一个任务,但底层其实分成两个完全不同的阶段。

阶段一:Prefill
你提交 prompt 后,模型必须先处理所有输入 token,才能开始生成。好处是,这一步可以并行。所有 token 的 Q、K、V 可以同时算出来,attention 也可以作为大型矩阵乘法来跑。
GPU 很喜欢这种活。矩阵乘矩阵是它的主场。这个阶段的瓶颈是算力吞吐,也就是 GPU 能不能尽快把这些乘法算完。
这个阶段对应的指标叫 Time to First Token,简称 TTFT。它就是你发出请求后,到第一个字出现在屏幕前的等待时间。
python
# Prefill: process the whole prompt in one shot
hidden = embed(prompt_tokens) + positions
for layer in model.layers:
Q, K, V = project(hidden) # for ALL tokens at once
hidden = attention(Q, K, V) + hidden
hidden = feedforward(hidden) + hidden
cache_kv(layer, K, V) # save for later
first_token = sample(project_to_vocab(hidden[-1]))
阶段二:Decode
第一个 token 出来后,模型切到另一种模式。比如要生成第 51 个 token,它只需要为这个新 token 计算 Q、K、V。前面 50 个 token 的 K 和 V 不会变,重新计算它们是浪费。

所以 decode 会变成一个 token 接一个 token 的循环:
python
# Decode: one token per iteration
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) # cached history + new
x = layer.forward(q, K_all, V_all, x) # attention + FFN, residuals
token = sample(project_to_vocab(x))
steps += 1
yield token
注意这里发生了什么变化。prefill 时,是一整批 query 矩阵去乘 key 矩阵。decode 时,只是一个 query 向量去看一整串历史 key。算术量小得多。
但 GPU 仍然要从显存里加载每一层的权重,也要加载缓存下来的 K 和 V。计算本身很小,内存读取反而成了主问题。芯片还有不少算力没用上,却在等显存把数据喂过来。
这就是为什么 decode 是 memory-bound,而 prefill 是 compute-bound。同一个模型,同一块硬件,性能瓶颈完全不同。
decode 阶段对应的指标叫 Inter-Token Latency,简称 ITL,也就是相邻两个 token 之间的间隔。ITL 低,模型才会让人觉得"流式输出很快"。
KV Cache:没有它,生成根本跑不动
上面循环里的 append_to_cache 才是关键。如果没有 KV Cache,生成 1,000 个 token 就意味着每一步都重新计算整段不断变长的序列。复杂度会变成平方级,慢到不可接受。
有了 KV Cache,模型只需要把 K 和 V 保存一次,后面一直复用。大概形状是这样:
python
# One KVCache per transformer layer
class KVCache:
def __init__(self):
self.K = None # all keys seen so far, shape [tokens, dim]
self.V = None # all values seen so far, shape [tokens, dim]
def append(self, k_new, v_new):
if self.K is None:
self.K, self.V = k_new, v_new # first 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 # full history so far
这个优化带来的速度提升很大。长输出里,5 倍甚至更高的加速很常见。
但代价也很明确:cache 要住在 GPU 显存里,并且每多生成一个 token 就变大一点。每一层都要保存自己的 K 和 V。以 13B 模型为例,一个 token 的 KV Cache 可能接近 1MB。4K token 的上下文,光 cache 就能吃掉大约 4GB VRAM。
所以长上下文又慢又贵,不是模型"脑子不够用",而是 cache 把显存吃光了。
对应的优化也都围着这件事转:把 cache 量化到 INT8 或 INT4;用 sliding window 丢掉窗口外 token;让多个 attention head 共享 K 和 V,也就是 grouped-query attention;或者像操作系统分页一样管理 cache,也就是 vLLM 背后的 PagedAttention。
前沿研究:直接把 cache 做小
量化和分页的前提是:KV Cache 是一笔固定成本,我们只能想办法少付一点。
DeepSeek V4 系列在 2025 年底预览时走得更激进:重新设计 attention,让 cache 从一开始就变小。
它的混合方案结合了两种压缩 attention 变体,一种 sparse,一种 dense,都运行在高度压缩过的 KV stream 上。在一百万 token 上下文里,V4-Pro 报告的 cache 大小约为上一代的 10%,每 token 计算量约为上一代的 27%。
重点不是某个具体架构,而是趋势:KV Cache 已经变成模型推理里绕不开的瓶颈。现在连 attention 本身都开始围着"怎么缩小 cache"重新设计,说明约束已经转移了。
如果你想看长上下文推理往哪走,可以读这份技术报告:DeepSeek-V4 paper。

Quantization:用更少 bit 换速度
训练需要更高精度,推理不一定需要。
生产部署里,很多模型不会用 FP32 跑,而是用 FP16 或 BF16。这样内存占用减半,在 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 从进入模型到流式返回,大概会走完这些步骤:
- Tokenize:文本变成整数 ID。
- Embed:ID 变成向量,位置信息被折进去。
- Prefill:每一层并行处理所有输入 token。这个阶段 compute-bound。KV Cache 被写入,第一个输出 token 出现。
- Decode loop:每生成一个新 token,就为它投影 Q,拿它去看缓存里的 K 和 V,跑 feed-forward,采样,再把新的 K 和 V 追加进 cache。这个阶段 memory-bound。
- Detokenize:token ID 被转回字符,流式显示到屏幕上。
现代 serving 框架,比如 vLLM、TensorRT-LLM 和 Text Generation Inference,会在这条循环外面再包几层工程优化:continuous batching,把多个用户的 token 交错放进同一个 GPU step;speculative decoding,让小模型先草拟 token,再交给大模型验证;再加上更聪明的显存管理。靠这些技术,单块 GPU 才能同时服务几十个并发用户。

这会改变你怎么看推理性能
把这张图拼起来以后,有几个实际结论会变得很清楚:
- 长 prompt 贵在 TTFT,长输出贵在 ITL。它们压的是两种不同资源。优化时先问用户真正感知到的是哪一个。
- context length 不是免费的。上下文翻倍,不只是计算翻倍,还会让 KV Cache 膨胀,压缩 batch size。
- quantization 是很高杠杆的旋钮。从 FP16 到 INT8,很多时候可以把延迟砍掉一大块,质量损失却很小。
- GPU utilization 会骗人。模型在 prefill 时可能把 GPU 打满,decode 时却只有 30%。这时候问题不是算力不够,而是显存带宽和 cache 太大。
transformer 架构吸走了太多注意力,但推理性能真正生死攸关的地方往往很朴素:内存布局、cache 管理、bit width、batch 调度。
工程的艺术,就是从手里这块硬件里榨出最多东西。
所以下次有人说"模型很慢",你应该先问一个问题:它是启动慢,还是流式输出慢?
如果你喜欢这类拆解,可以在评论里告诉我。这是一个信号,说明后面应该继续写更多这样的内容。
感谢阅读。