Skip to content

原文链接https://pcno429fb6c3.feishu.cn/docx/GDXgdamGNohoskxKJ8fcR8zPnec
版权声明:本文经授权转载,版权归原作者所有。

一次 LLM 推理到底发生了什么?(完整版)

LLM推理流程图

你按下回车后,模型其实在做两件完全不一样的活:先是把整个 prompt 一次性吞下并填好 KV 缓存的 prefill(预填充)——这一段是计算受限,瓶颈是 GPU 算力,对应指标是 首 token 时延(TTFT);然后是一次只生成一个 token、不停追加 KV 缓存的 decode(解码)——这一段是内存受限,瓶颈是显存带宽,对应指标是 token 间时延(ITL)。文章按"先讲直觉再上代码"的节奏带你走过完整管线:分词把文本切成 token、嵌入把 ID 变成向量、注意力让每个 token 决定自己要看哪些上下文、前馈网络做加工、最后采样出下一个 token。KV 缓存是让长输出可行的关键优化,也是当下最大的成本和瓶颈——所以才会催生 INT8/INT4 量化、滑动窗口、分组查询注意力(GQA)、PagedAttention,乃至 DeepSeek-V4 那种"重新设计注意力让缓存从一开始就很小"的激进路线。三条工程直觉值得记住:长 prompt 拖 TTFT、长输出拖 ITL;上下文长度从来都不是免费的;量化是收益最高的那个旋钮。下次再有人说"模型很慢",先问一句——慢在启动,还是慢在流式输出?

从第一性原理出发

看看你按下回车到流式输出之间究竟经过了什么:分词、嵌入、注意力、prefill / decode 的两段拆分、KV 缓存,以及量化。

你输入一段 prompt。几百毫秒后,文字开始一个一个 token 地流回来。看起来很简单。其实不是。

从你按下键盘到第一个 token 出现在屏幕上,中间走过的是现代计算里被打磨得最仔细的管线之一。最有意思的一点是:模型为了回答你这一句话,其实在做两件完全不一样的工作,瓶颈不一样、特点不一样,却跑在同一张 GPU 上、同一次请求里。

看懂这一层,你再也不会用以前那种眼光看 generate() 调用了。

心智模型

LLM 是一个用来预测下一个 token 的神经网络。就一个 token。然后它把这个 token 接到你 prompt 的末尾,再预测下一个。如此循环。

就这么简单。整个循环就这一句话。

有意思的问题是:它怎么预测下一个 token?以及,为什么第二个 token 比第一个 token 出得快得多?

第 1 步:你的文本变成数字

神经网络读不懂英文。它读的是向量。所以 prompt 进来后,第一件事是分词(tokenization):把你的文本切成一块一块,再给每一块分配一个整数 ID。

主流 LLM 用的是字节对编码(Byte Pair Encoding,BPE)。思路是:从原始字符开始,把出现得最频繁的相邻字符对反复合并,最终凑出一个大约 5 万项的词表。常见词比如 the 占一个 token;像 unhappiness 这种偏门词,会被切成 un + happi + ness 三块。

python
"How does inference work?" # ids -> [2437, 1374, 32278, 670, 30]

这一步比很多人意识到的要重要。在分词器训练数据中权重不足的语言,会被切成更多的小块——也就是更多的 token——同样一句话花的钱更多、响应也更慢。

第 2 步:每个 token 变成一个向量

每个整数 ID 都会去查一张超大的矩阵——嵌入表(embedding table)。如果你的模型词表有 5 万项、隐藏维度是 4096,这张表的形状就是 [50000, 4096]。挑出对应的一行,就拿到一条向量。

python
# embedding_table 形状: [vocab_size, hidden_dim]
# 形状: [num_tokens, 4096]

这些向量不是随机的。训练过程中,模型会反复挪动它们,让语义相近的 token 在这个 4096 维空间里也彼此靠近。king 和 queen 是邻居;python 和 snake 在某条轴上是邻居,python 和 javascript 又在另一条轴上是邻居。

嵌入层也是位置信息被注入的地方——因为注意力本身并不知道哪个 token 在前、哪个在后。现代模型用的是诸如旋转位置编码(RoPE)这样的方案,根据 token 在序列中的位置去旋转它的向量。

第 3 步:堆叠的注意力层

真正的活儿从这里开始。你这串向量会被送进一摞 Transformer 层,常见是 32 层甚至更多,一层一层串过去。每一层做的事大致一样:

  • 用自注意力(self-attention)在 token 之间互相搬运信息。
  • 用前馈网络(feed-forward network)在每个 token 内部加工信息。

自注意力是最值得搞懂的一块。对每一个 token,这一层会通过乘以三个学习好的权重矩阵,得到三个新向量:

python
# x 是这一层的输入,形状 [num_tokens, hidden_dim]
# queries(查询)
# keys(键)
# values(值)

于是你就有了每个 token 的三种"视角"。诀窍是:每个 token 用自己的查询去匹配其他所有 token 的键,匹配得越紧,就把对方的值取得越多过来。

python
# scores: 每个 token 对其他每个 token 的关注度
# 让 softmax 数值稳定
# 每行和为 1,对应一个 token

这个过程的视觉化大致长这样:

注意力机制示意图

魔法就在这里。每个 token 自己决定要看哪些上下文,看一圈之后把有用的内容拽过来。把 32 层这种东西叠起来,模型就能在跨越几千个 token 的范围里追踪指代。

注意力之后,每个 token 的向量再过一个小型的两层前馈网络——这才是模型大部分**真正的"知道"**所在的地方。注意力负责搬运信息,前馈网络负责加工信息。

第 4 步:预测下一个 token

最后一层结束后,模型取出最后一个位置的向量,把它投影回词表大小,再经过 softmax,就得到了所有可能的下一个 token 上的一个概率分布。从这个分布里采样一下,你就拿到了第一个生成的 token。

接下来是有意思的部分。

没人告诉你的"两段式"

生成一段 200 token 的回答,不是一项任务,而是两项——它们在底层长得完全不一样。

第一段:预填充(prefill)

你提交一个 prompt 后,模型必须先把你输入的全部 token 处理完,才能开始生成。好消息是:这一步可以并行做。每个 token 的 Q、K、V 同时算出来,注意力作为一次大型的矩阵乘矩阵跑掉。

GPU 喜欢这种活。矩阵乘矩阵就是它生来要做的。这一段的瓶颈是纯算力吞吐——GPU 利用率被钉在很高的位置上,硅片以最高速度做算术。

这一段对应的指标是 首 token 时延(Time to First Token,TTFT)——你按下回车到屏幕上蹦出第一个词之间那段空白时间。

python
# 预填充: 一次性把整个 prompt 跑掉
for layer in model.layers:
    # 一次算完所有 token
    q, k, v = layer.compute_qkv(prompt_vectors)
    # 存下来给后面用
    kv_cache[layer].append(k, v)

第二段:解码(decode)

第一个 token 一出来,模型就切换模式。要生成第 51 个 token,它只需要算这一个 token 的 Q、K、V。前面那 50 个 token 的 K、V 没变过,重新算就是浪费。

于是模型进入一个一次一个 token 的循环:

python
# 解码: 每次循环只生成一个 token
current_token = first_generated_token
while current_token != EOS and len(output) < max_length:
    for layer in model.layers:
        # 历史缓存 + 新算
        q = layer.compute_q(current_token_vector)
        k_new, v_new = layer.compute_kv(current_token_vector)
        k_all, v_all = kv_cache[layer].append(k_new, v_new)
        # attention + FFN + 残差
        current_token_vector = layer.forward(q, k_all, v_all)
    current_token = sample(current_token_vector)
    yield current_token

注意发生了什么变化。原本是"查询矩阵 × 键矩阵",现在变成了"单个查询向量 × 键矩阵"。算术量一下子变得很小。

但 GPU 仍然得把每一个权重矩阵、每一份缓存好的 K 和 V 从显存里拉过来才能完成这一点点计算。瓶颈翻转了:芯片有大把算力却闲着,干等着内存把下一块数据送过来。

这就是为什么——解码是内存受限的,预填充是计算受限的。同一个模型、同一台机器,性能特征却完全不同。

这一段对应的指标是 token 间时延(Inter-Token Latency,ITL):连续两个 token 流出来之间的间隔。低 ITL 才是让一个模型"感觉快"的关键。

KV 缓存:让这一切变得可行的优化

上面那行 caches[layer].append(k, v) 是真正的承重墙。没有它,生成一段 1000 token 的回复就意味着每一步都要对越来越长的整段序列重新算一次注意力——平方复杂度,慢到难以忍受。

有了它,每一步的 K 和 V 只算一次,之后一直复用。大致是这样:

python
# 每一层 Transformer 都有一份自己的 KVCache
class KVCache:
    def __init__(self):
        self.keys = None   # 至今所有的 keys, 形状 [tokens, dim]
        self.values = None # 至今所有的 values, 形状 [tokens, dim]
    
    def append(self, k_new, v_new):
        if self.keys is None:
            self.keys = k_new      # 第一个 token
            self.values = v_new
        else:
            self.keys = torch.cat([self.keys, k_new], dim=0)
            self.values = torch.cat([self.values, v_new], dim=0)
        return self.keys, self.values  # 至今为止的全部历史

加速幅度非常可观。长输出场景下,5 倍以上是常态。但代价是:缓存住在 GPU 显存里,并且每多一个 token 就多一份。每一层 Transformer 都各自存一份 K 和 V。一个 130 亿参数级别的模型,大约每个 token 要吃掉 1MB 的 KV 缓存——一段 4K token 的上下文,光是缓存就要占 4GB 显存(VRAM)。

这就是为什么长上下文又慢又贵。不是模型不够聪明,而是缓存装不下了。

修法都很有创意:把缓存量化到 INT8 或 INT4;用滑动窗口扔掉太老的 token;让多个注意力头共享同一份 K 和 V(也就是分组查询注意力,GQA);或者像操作系统给内存分页那样给缓存分页(PagedAttention,vLLM 背后的关键技巧)。

前沿研究:把缓存本身变小

量化和分页都是把 KV 缓存当作一笔固定开销来管。DeepSeek 在 2025 年末预览的 V4 系列走得更激进:重新设计注意力,让缓存从一开始就很小。

他们的混合方案把两种压缩注意力变体(一种稀疏、一种稠密)组合在一起,都跑在被大幅压缩过的 KV 流上。在百万 token 上下文下,V4-Pro 报告的缓存大小约为前一代的 10%,每 token 算力消耗约为 27%。

可借鉴的不是那个具体架构,而是另一件事:KV 缓存已经成了整个领域围着它做优化的核心瓶颈。当注意力本身都被改造来"少占缓存"时,你就知道约束已经换了一边。

如果你关心长上下文推理走向哪里,这篇值得读:DeepSeek-V4 paper

量化:拿比特换速度

训练需要精度,推理不需要。

绝大多数生产部署都不再用 FP32,而是 FP16 或 BF16。这一步直接把内存砍半,并且在 Tensor Cores 上把吞吐大致翻倍。更激进的方案再走一步,把权重量化到 INT8 甚至 INT4。

账很直接。一个 70 亿参数的模型占用:

  • FP32 下 28GB
  • FP16 下 14GB
  • INT8 下 7GB
  • INT4 下 3.5GB

最后一行就是为什么你能在一张笔记本 GPU 上跑 7B 模型。GPTQ、AWQ 这类方法会按通道挑选缩放系数,让有损压缩对质量的伤害降到最低。做得好的话,INT4 在大多数 benchmark 上能保持在原始模型一两个百分点之内。

把流程串起来

一次 prompt 的完整旅程:

  1. 分词:文本变成整数 ID
  2. 嵌入:ID 变成向量,位置信息也在这一步注入
  3. 预填充:每一层在所有输入 token 上并行跑一遍,受算力约束。KV 缓存被填满。第一个输出 token 蹦出来
  4. 解码循环:对每个新 token,算它的 Q,配合缓存中的 K、V 做注意力,过前馈网络,采样。再把新的 K、V 追加进缓存。受内存带宽约束
  5. 反分词:token ID 被映射回字符,流式发到你的屏幕上

vLLMTensorRT-LLM、Text Generation Inference 这类现代服务框架在这套循环外面又包了一层:连续批处理(continuous batching,多个用户的 token 在同一次 GPU 步骤里被交织处理)、投机解码(一个小模型先草稿出 token,大模型只做验证)、以及精巧的显存管理。这就是为什么一张 GPU 能同时服务数十个并发用户。

这件事应该改变你的工程直觉

把这张图看清楚之后,几个实战体感会变得不一样:

  • 长 prompt 拖 TTFT,长输出拖 ITL。它们压力点不同。优化你的用户真正能感受到的那一个
  • 上下文长度不是免费的。翻倍上下文不仅翻倍算力,还会让 KV 缓存膨胀,把你能并行的 batch size 挤瘦
  • 量化是收益最高的旋钮。FP16 → INT8 经常能把时延砍半,质量损失几乎可忽略
  • GPU 利用率会骗人。一个在 prefill 阶段 100% 占用的模型,到了 decode 阶段可能只有 30%。要修的不是再加一块算卡,而是更快的显存或者更小的缓存

Transformer 架构总是抢走所有眼球,但推理性能的生死线却在那些不起眼的地方:内存布局、缓存管理、比特宽度。这门手艺的本质,就是从你手上的硬件里挤出最多的东西。

下次再有人跟你抱怨"我的模型很慢",你就知道第一个该问的问题是:它是慢在启动,还是慢在流式输出?

参考资料