大模型推理全解析:从Prompt到第一个Token的完整流程与优化
1. 从“你好”到“世界”:一次请求的奇幻漂流
当你向一个大模型输入一句“你好,世界”,然后按下回车,屏幕上的光标开始闪烁,几秒后,模型开始逐字逐句地吐出回答。这个从你输入到模型输出第一个字符之间的“黑盒”过程,远比你想象的要复杂和精妙。它不是一个简单的“输入-处理-输出”流水线,而是一场涉及数据转换、并行计算、概率采样和资源调度的精密协同作战。今天,我们就来彻底拆解这个过程,看看你的一个简单请求,是如何在模型的“大脑”里掀起一场风暴,并最终凝结成第一个 token 的。
这个过程的核心,可以概括为一条主线: 文本的“降维”与“升维”之旅 。你的自然语言(高维、连续、富含语义)首先被“降维”成模型能理解的离散数字序列(Token ID),然后经过模型内部复杂的、由多层注意力机制构成的“升维”计算,最终再“降维”为一个概率分布,从中采样出第一个 Token。理解这个过程,不仅是理解大模型工作原理的钥匙,更是进行高效推理部署、Prompt 工程和性能优化的基础。无论你是开发者、研究者,还是对 AI 内部运作感到好奇的爱好者,跟随这次“漂流”,你都将获得一幅清晰的内部地图。
2. 旅程起点:Prompt 的数字化与向量化
在你按下回车键的瞬间,你的 Prompt 还只是一串人类可读的字符。模型要理解它,第一步是将其转化为自己能处理的“语言”。
2.1 Tokenization:从字符到数字 ID 的精确切割
这个过程叫做 Tokenization(分词/标记化) 。它远不止是简单的按空格或字符分割。现代大模型(如 GPT、LLaMA 系列)通常采用基于 Byte Pair Encoding (BPE) 或类似算法(如 SentencePiece)的子词分词器。
为什么是子词,而不是单词或字符?
- 字符级 :计算效率极低,序列过长,且丢失了常见的语义组合信息(如“un-”前缀,“-ing”后缀)。
- 单词级 :词汇表会无限膨胀(新词、拼写错误、复合词),且无法处理未登录词(OOV)。
- 子词级 :一个绝佳的折中。它通过统计学习,将常见词作为一个整体(如“hello” -> token_id: 12345),将罕见词或复杂词拆分为有意义的子单元(如“tokenization” -> “token”, “ization”)。这既控制了词汇表大小(通常在3万到10多万),又具备了强大的泛化能力。
实操中的关键细节:
-
特殊 Token 的插入 :分词器会自动在 Prompt 前后添加特殊的控制 Token。最常见的是:
-
<bos>(Beginning of Sequence) 或<s>:标记序列的开始。它为模型提供了一个清晰的“起点”信号。 -
<eos>(End of Sequence) 或</s>:标记序列的结束。在推理时,它通常由模型在生成完成后输出。 - 对于对话模型,还会有
[INST]、<<SYS>>等角色标记,用来区分系统指令、用户输入和助手回复。这些特殊 Token 和普通词一样,有自己唯一的 ID。
例如,你的 Prompt “你好,世界” 经过分词和添加特殊标记后,可能变成:
[<bos>, “你”, “好”, “,”, “世”, “界”, <eos>]对应的 ID 序列。 -
-
长度限制与截断 :每个模型都有其 上下文窗口(Context Window) 限制,比如 4K、8K、32K、128K tokens。分词器会检查 Prompt 转换后的 Token 序列长度。如果超出限制,通常有两种处理策略:
- 截断(Truncation) :丢弃超出部分(从开头或结尾)。
- 拒绝并报错 :直接返回错误,提示输入过长。 在生成第一个 Token 之前,这个长度检查就必须完成,因为它决定了后续所有计算张量的形状。
注意 :不同的分词器对同一文本的切分结果可能不同。例如,“Hello world!” 在某个分词器下可能是
[“Hello”, “ world”, “!”],而在另一个下可能是[“Hell”, “o”, “ world”, “!”]。这直接影响了输入序列的长度和模型的理解细微差别。因此,在对比不同模型或进行提示工程时,了解其分词特性至关重要。
2.2 Embedding 查找:为每个 ID 赋予灵魂
得到一串冰冷的数字 ID(如 [1, 305, 1023, 12, 789, 2345, 2] )后,下一步是让它们“活”起来,即转化为富含语义信息的连续向量。这就是 Embedding(词嵌入) 层的工作。
你可以把 Embedding 层想象成一个巨大的“字典”或查找表。这个表的行数等于词汇表大小(V),列数等于模型的隐藏维度(D,例如 4096、8192)。每个 Token ID 就是这个字典的“页码”,通过查找,我们得到对应页码下的一个 D 维向量。
数学表达很简单: Embedding_Matrix ∈ R^(V×D) Token_IDs = [id_1, id_2, ..., id_L] Input_Embeddings = Embedding_Matrix[Token_IDs, :] ∈ R^(L×D)
这里的 L 就是当前 Prompt 的 Token 序列长度,D 是模型的隐藏维度。
为什么这一步至关重要?
- 语义初始化 :这个查找得到的向量,是在海量文本上预训练好的,它已经编码了该 Token 丰富的语义和语法信息。例如,“国王”和“王后”的向量在“性别”维度上方向相反,但在“王室”维度上相近。
- 计算的基础 :后续所有复杂的注意力计算、前馈网络变换,都是作用在这些连续的、高维的向量表示上,而不是原始的离散 ID 上。
一个常被忽略的细节:位置编码(Positional Encoding) 语言是有顺序的。“猫追老鼠”和“老鼠追猫”意思完全不同。但到目前为止,我们的 Embedding 只包含了词汇信息,没有顺序信息。因此,我们必须显式地注入位置信息。
- 绝对位置编码(如 Sinusoidal) :为序列中每个位置(1, 2, ..., L)生成一个唯一的 D 维向量,然后加到对应的 Token Embedding 上。这是 Transformer 原论文的方法。
- 相对位置编码(如 RoPE, ALiBi) :现代大模型更常用。它不直接加一个绝对位置向量,而是在注意力计算过程中,根据 Token 之间的相对距离来调整注意力分数。例如 RoPE(旋转位置编码) ,通过旋转查询(Query)和键(Key)向量的方式来融入相对位置信息,被 LLaMA、GPT-NeoX 等模型广泛采用。
至此,我们的 Prompt 已经从一串字符,变成了一个形状为 (L, D) 的、包含了丰富语义和精确位置信息的 张量矩阵 。真正的“思维”计算,即将开始。
3. 核心计算:Transformer 解码器的前向传播
现在,承载着 Prompt 信息的 (L, D) 张量,将进入模型的核心——由 N 个完全相同的 Transformer 解码器层堆叠而成的深度网络。每一层都在对信息进行提炼、融合和转换,目标是计算出序列下一个位置(即第 L+1 个位置)应该是什么 Token 的概率分布。
3.1 一层解码器的内部流水线
我们以生成第一个 Token(即预测序列第 L+1 个位置)为例,深入一层解码器内部。注意,在标准的自回归生成中,为了效率,我们通常使用 KV Cache(键值缓存) 技术。但为了理解最本质的计算过程,我们先从最朴素的、没有缓存的视角来看。
假设当前输入是经过前面所有层处理后的隐藏状态 H ∈ R^(L×D) ,它进入第 i 层:
第一步:掩码多头自注意力(Masked Multi-Head Self-Attention) 这是 Transformer 的灵魂。它的目标是让序列中的每个 Token,都能根据之前的所有 Token(不包括它自身和之后的,因为是解码器)来更新自己的表示。
- 线性投影 :将输入
H通过三个不同的权重矩阵W_Q, W_K, W_V投影,得到查询(Query)、键(Key)、值(Value)张量:Q, K, V = H @ W_Q, H @ W_K, H @ W_V。形状均为(L, D)。 - 分头(Multi-Head) :将
Q, K, V在特征维度 D 上切分成h个头(例如 D=4096, h=32,则每个头维度为 128)。这样模型可以在不同的表示子空间里并行地关注不同的信息。 - 计算注意力分数(Scaled Dot-Product Attention) :
- 对于每个头,计算
Scores = Q_head @ K_head^T / sqrt(d_k),其中d_k是每个头的维度。Scores的形状是(L, L),它表示每个 Token 对所有 Token 的“关注程度”。 - 关键:应用因果掩码(Causal Mask) 。因为我们是生成式模型,在预测第
t个 Token 时,它不应该“看到”第t+1及之后的 Token。所以,我们将Scores矩阵的上三角部分(不包括对角线)填充为负无穷(-inf),这样在后续的 softmax 中,这些位置的权重就会变成 0。 - 对每一行应用 softmax 归一化,得到注意力权重
Weights = softmax(Scores, dim=-1)。 - 加权求和:
Attention_Output = Weights @ V_head。形状为(L, d_k)。
- 对于每个头,计算
- 合并头(Concat) :将
h个头的输出在特征维度拼接起来,形状恢复为(L, D)。 - 输出投影 :通过一个线性层
W_O将合并后的结果投影回 D 维,得到该注意力子层的最终输出A。
第二步:残差连接与层归一化(Add & Norm) 将注意力子层的输出 A 与原始的输入 H 相加(残差连接),然后通过一个 层归一化(LayerNorm) 。即: H1 = LayerNorm(H + A) 。残差连接有助于缓解深度网络中的梯度消失问题,让模型更容易训练。
第三步:前馈网络(Feed-Forward Network, FFN) 这是一个简单的两层全连接网络,通常中间有一个扩展维度(例如,D=4096,中间层维度为 11008)。 FFN(H1) = gelu(H1 @ W1 + b1) @ W2 + b2 其中, gelu 是激活函数。FFN 为每个位置的 Token 表示进行独立的、复杂的非线性变换。
第四步:再次残差连接与层归一化 将 FFN 的输出与 H1 相加,再做一次层归一化: H_out = LayerNorm(H1 + FFN(H1)) 。这就是第 i 层解码器的最终输出,它将作为下一层的输入。
这个过程在 N 个解码器层中依次进行,信息被一层层地提炼和抽象。
3.2 聚焦于最后一个位置:为生成做准备
经过所有 N 层解码器的处理,我们得到了最终一层的输出隐藏状态 H_final ∈ R^(L×D) 。这个张量包含了整个 Prompt 序列经过深度理解后的综合信息。
但是,我们只关心 下一个 Token 是什么。在自回归生成中,我们实际上只需要序列 最后一个位置 的隐藏状态,因为它凝聚了前面所有 L 个 Token 的信息,用于预测第 L+1 个 Token。
所以,我们从 H_final 中取出最后一行的向量: h_last ∈ R^(1×D) 。这个 h_last 向量,就是模型基于整个 Prompt 上下文,为“下一个词”生成的“思想结晶”。
4. 概率的诞生:从向量到第一个 Token
现在,我们有了一个 D 维的向量 h_last ,它代表了模型对下一个 Token 的“想法”。如何将这个连续空间的“想法”转换回离散的词汇表中的一个 Token 呢?
4.1 线性投影与 Logits 计算
这通过一个简单的、但非常关键的 线性层(也称为语言模型头,LM Head) 来完成。这个层的权重矩阵 W_lm ∈ R^(D×V) ,其中 V 是词汇表大小。
计算过程: logits = h_last @ W_lm 。结果 logits 是一个长度为 V 的向量。
什么是 Logits? 你可以把它理解为每个候选 Token 的“原始分数”或“未归一化的置信度”。数值越大,模型认为该 Token 出现在下一个位置的可能性就越高。但它还不是概率,因为所有 logits 的和不是 1,且可能包含负值。
4.2 Softmax:将分数转化为概率分布
接下来,我们对 logits 向量应用 softmax 函数,将其转换为一个合法的概率分布 P ∈ R^(V) 。
P_i = exp(logits_i) / sum(exp(logits_j)) for j in 1 to V
现在, P 是一个长度为 V 的概率向量,其中每个元素 P_i 代表了词汇表中第 i 个 Token 作为下一个输出 Token 的概率。所有 P_i 的和为 1。
4.3 采样策略:决定性的最后一步
拿到概率分布 P 后,我们不是简单地选择概率最大的那个 Token(贪心搜索)。那样会导致生成结果重复、枯燥。现代大模型生成通常采用更灵活的采样策略:
-
温度调节(Temperature Scaling) : 在 softmax 之前,将 logits 除以一个温度参数
T。T = 1:保持原分布。T < 1(如 0.8):放大高概率和低概率之间的差距,分布变得更“尖锐”,输出更确定、更保守。T > 1(如 1.2):缩小差距,分布变得更“平坦”,输出更随机、更有创造性。 公式变为:P_i = exp(logits_i / T) / sum(exp(logits_j / T))。
-
Top-k 采样 : 只从概率最高的 k 个 Token 中构建新的概率分布,然后从中采样。这排除了那些概率极低的荒谬选项。
-
Top-p(核)采样 : 从概率最高的 Token 开始累积,直到累积概率超过预设值 p(如 0.9),然后只从这部分 Token 中构建新分布并采样。这种方法能动态适应概率分布的陡峭程度。
-
重复惩罚(Repetition Penalty) : 为了减少重复,在采样前,降低那些已经在当前上下文中出现过的 Token 的 logits 值。
最终,根据设定的策略(例如, temperature=0.8, top_p=0.9 ),从调整后的概率分布 P_adjusted 中随机采样,得到下一个 Token 的 ID。
这个 ID,就是模型输出的 第一个 Token 。它会被立刻转换回人类可读的字符(通过反向查找分词器的词汇表),并显示在你的屏幕上。
5. 工程优化:让“第一个Token”更快到来
从用户感知的“延迟”角度看,从输入结束到看到第一个 Token 的时间(Time To First Token, TTFT)至关重要。为了加速这个过程,推理引擎做了大量优化。
5.1 KV Cache:避免重复计算的利器
回顾第3章,在计算第 L+1 个 Token 时,我们需要 Prompt 中所有 L 个 Token 的 Key 和 Value 矩阵。在朴素实现中,每生成一个新 Token,都需要为整个历史序列(长度+1)重新计算一遍所有层的 Q, K, V,这是 O(n²) 的复杂度。
KV Cache 的核心思想是:缓存(Cache) 。
- 在 预填充(Prefill)阶段 (处理用户 Prompt 时),我们计算并保存 Prompt 中每个 Token 在所有层、所有注意力头中的 Key 和 Value 向量。这是一个形状为
[层数, 序列长度, 头数, 头维度]的缓存张量。 - 在 解码(Decoding)阶段 (生成每个新 Token 时),我们只需要:
- 用最新的隐藏状态(即上一个输出的 Token 的嵌入)计算当前步的 Q, K, V。
- 将当前步新算出的 K, V 追加 到对应层的 KV Cache 中。
- 注意力计算时,Query 是当前步的(形状
(1, ...)),而 Key 和 Value 直接使用整个缓存(形状(当前总长度, ...))。 这样,每个生成步只需要为最新的一个 Token 计算 QKV,并做一次注意力计算,复杂度降为 O(n)。这极大地加快了首个及后续 Token 的生成速度。
5.2 Flash Attention:极致的内存与计算优化
即使有了 KV Cache,注意力计算中的 softmax(QK^T)V 操作仍然需要将中间矩阵 QK^T (形状 (1, L) )写回昂贵的高速内存(HBM),然后再读回来进行 softmax 和乘 V,这被称为“内存墙”问题。
Flash Attention 是一种革命性的 IO 感知精确注意力算法。它通过:
- 平铺(Tiling) :将 Q, K, V 分块加载到快速的片上内存(SRAM)进行计算。
- 重计算(Recomputation) :在反向传播时,不存储巨大的中间注意力矩阵,而是根据存储的少量信息重新计算。这在训练时节省大量内存。
- 对于推理(尤其是生成阶段),Flash Attention 及其变种(如 FlashDecoding )通过优化计算流程,减少了 GPU 不同级别内存间的数据搬运,从而显著提升了注意力计算的吞吐量,降低了延迟。
5.3 连续批处理与推测解码
在服务端,为了提升 GPU 利用率,会采用 连续批处理(Continuous Batching) 。不同用户的请求(可能处于生成的不同阶段)被动态地打包成一个批次进行计算。这要求推理引擎能高效管理不同序列的 KV Cache。
更前沿的优化如 推测解码(Speculative Decoding) ,其核心是用一个更小的“草稿模型”快速生成一串候选 Token,然后用原始大模型并行地对这些候选进行验证,一次性接受多个正确的 Token。这能显著降低每个生成 Token 的平均延迟。不过,对于“第一个 Token”的生成,推测解码通常不直接起作用,因为它的优势体现在连续生成阶段。
6. 从理论到故障:实践中可能遇到的问题
理解了完美流程,我们更要看看现实中可能出错的环节。当你的请求迟迟没有响应,或者返回了奇怪的第一个 Token 时,问题可能出在哪里?
6.1 输入处理阶段的常见坑
-
分词不一致导致意外输出 :
- 场景 :你在本地测试时 Prompt 工作良好,部署到线上服务后输出却南辕北辙。
- 排查 :检查线上和线下使用的模型版本、分词器文件是否完全一致。即使是同一个模型家族(如 LLaMA2-7B),不同机构发布的版本其分词器可能有细微差别。一个额外的空格、不同的特殊 Token 定义,都可能导致分词结果不同,进而影响 Embedding 查找和模型理解。
- 实操心得 :始终将分词器与模型权重一起打包、版本化管理。在构建服务时,将分词过程作为关键环节进行单元测试,确保输入输出的确定性。
-
位置编码溢出或未对齐 :
- 场景 :当输入长度接近或超过模型训练时的最大位置(如 2048)时,模型输出可能变得毫无逻辑或崩溃。
- 排查 :对于使用 RoPE 的模型,需要确认推理代码是否正确实现了相对位置的计算。对于长上下文模型,要确认其是否支持外推(extrapolation)或使用了如 NTK-aware 等缩放技术来适应更长的序列。
- 实操心得 :在部署前,务必用不同长度的输入(特别是超长输入)对模型进行压力测试。仔细阅读模型发布方的说明,了解其位置编码的细节和长度限制。
6.2 计算与采样阶段的“玄学”问题
-
采样参数设置不当 :
- 症状 :输出过于天马行空、胡言乱语,或者相反,极其保守、重复。
- 诊断 :检查
temperature、top_p、top_k等参数。过高的温度(>1.5)或过低的 top_p(<0.5)可能导致采样到低概率的奇怪 Token。温度设为0(贪心搜索)则容易导致重复循环。 - 经验值 :对于需要创造性写作的任务,可以尝试
temperature=0.7~0.9, top_p=0.9。对于需要事实性、确定性的问答,可以尝试temperature=0.1~0.3, top_p=0.95。没有银弹,需要针对具体任务进行 A/B 测试。
-
Logits 处理器(Logits Processor)的影响 :
- 许多推理框架支持在 softmax 之前对 logits 进行修改,例如强制某些 Token 不出现(
NoBadWordsLogitsProcessor)、进行重复惩罚等。 - 排查 :如果输出完全避开了某个合理词汇,或者重复惩罚过于严厉导致输出截断,需要检查这些处理器是否被正确配置和启用。
- 许多推理框架支持在 softmax 之前对 logits 进行修改,例如强制某些 Token 不出现(
-
数值精度与随机种子 :
- 在 GPU 上,float16 和 bfloat16 的混合精度计算是常态,但极端情况下可能存在数值不稳定。
- 采样是随机的。为了复现结果,需要设置固定的随机种子(
seed)。在分布式或流式生成中,确保随机数生成器的状态管理正确。
6.3 系统与性能瓶颈
-
长 Prompt 导致的 TTFT 过高 :
- 根因 :预填充阶段(计算整个 Prompt 的 KV Cache)的计算量和内存访问量与 Prompt 长度 L 成正比。一个数万 Token 的文档作为 Prompt,其首次计算耗时可能长达数秒甚至数十秒。
- 优化思路 :
- Prompt 压缩/摘要 :在送入大模型前,先用一个小模型或算法对超长 Prompt 进行摘要。
- 流式处理 :对于极长文本,可以考虑边输入边进行部分预填充,但实现复杂。
- 使用更高效的注意力算法 :如 FlashAttention-2,能有效降低长序列预填充的计算时间。
-
内存瓶颈与 OOM :
- KV Cache 是内存消耗大户。其大小约为
2 * 层数 * 头数 * 头维度 * 序列长度 * 批次大小 * 精度字节数。 - 对策 :
- 量化(Quantization) :将 KV Cache 和模型权重从 FP16 量化到 INT8 甚至 INT4,可以大幅减少内存占用。
- 分页注意力(PagedAttention) :像操作系统管理内存一样管理 KV Cache,允许非连续存储,极大提高内存利用率,这是 vLLM 等高性能推理引擎的核心技术。
- 动态批处理 :根据当前 GPU 内存情况,动态调整批次大小。
- KV Cache 是内存消耗大户。其大小约为
当你理解了从 Prompt 到第一个 Token 的完整链条,以及其中每个环节的优化和陷阱,你就能更好地驾驭大模型,设计出更高效的推理服务,并快速定位和解决生成过程中出现的问题。这个过程不仅仅是模型的前向传播,更是算法、系统、工程三者的深度结合。
更多推荐
所有评论(0)