从输入到输出:大模型推理全流程复盘(含纠错清单)

2026-11-12 问答整理。不绑定具体框架,只讲模型本身怎么从「输入」走到「输出一个字」。 主线按单头 讲(先把多头放一边),真实多头差异放在最后一节。 记号约定:不用字母 O(期间专门确认过它是数字 0 还是字母 O ------ 答案是大写字母 O ,取自 Output,不是数字 0),统一写「输出向量 」。Q / K / V 保留。 为了数字能一路跟到底,用一个简化模型:向量宽度 128、共 4 层、词表 5 万。真实 Qwen3-0.6B 是 1024 宽 / 128 每头 / 28 层 / 词表 151936,流程完全一样。
同期我还把这套内容整理成了一篇更体系化的教程(含 9 张图解和完整纠错清单),可配合食用:《FlashDecoding 零基础学习总结:从 attention 三段式到 split-KV 合并》


全景(一段话先把路铺好)

把 prompt 的所有 token 一次性加载进 transformer → 一次算出全部 QKV、把 KV 存进 block(KV cache)→ 产出第一个字 → 进入 decode,一个字一个字往外吐 → 每一轮:新字查 embedding 表得到输入向量 → 过 4 层(每层都做一次 attention + FFN + 两次残差)→ 最后一层得到最终状态向量 → 跟词表矩阵相乘得到 5 万个分数 → softmax 变成概率 → top-k 抽样或取最高分 → 得到 1 个 token id → 输出这个字 → 这个 id 再查 embedding 表,进入下一轮 decode。

FlashDecoding 只插在「每层 attention 读历史 KV」那一下,别的地方一概不动。


一、Prefill:把所有 token 一次性加载进去

我当时的说法:

「prefill 阶段把 token 全部加载进 transformer 里面,然后一次性把所有的 token 生成 qkv,然后把 kv 存在 block 里面,这个时候每个 token 可以分开算 qkv,所以这个时候是计算密集型,npu 的计算资源能够得到极大的利用!」

对的部分:一次性吃进全部 prompt、批量算出 QKV、KV 存进分页 block(KV cache)、Q 的 token 数多所以算力能打满。这个判断抓得很准,是整个问题的起点。

要补三句:

  1. prefill 不只是"生成 QKV",它同样要做 attention(Q 和 K 算相关性,带因果掩码,只能看自己和之前的),只不过 Q 有几百上千行,切得开。
  2. QKV 是一次大矩阵乘批量算出来的,不是"每个 token 分开算"------但"可并行"这个直觉没错。
  3. prefill 也有 KV cache 写入,而且是一次写一大批。

二、Decode:一个字一个字往外吐

我当时的说法:

「prefill 阶段结束后产出了第一个 token,然后在 decode 阶段产生 qkv,这个时候 q 需要和前面生成的每一个 k 和 v 进行计算然后得到一个 logics 然后在进行 softmax,根据这个 softmax 选出合适的 token。在这个过程中 npu 里装着权重计算只是 npu 的一个单元在用 q 和前面的所以 k 进行计算看看有没有关系,npu 的其他单元并没有工作!」

这段方向对、细节错了好几处,逐条拧:

  • 「q 和前面每一个 k 和 v 计算 」→ 准确是三步:q 跟每个 k 打分 → 分数归一化成权重 → 用权重乘对应的 v 加权求和。K 是打分用的钥匙,V 是被加权的内容。
  • 「得到一个 logics 然后 softmax,根据这个 softmax 选出 token 」→ 这里把两层 softmax 混成一层了,是最关键的一个错 。
    • attention 内部 的 softmax:给每个历史位置打权重,然后用权重把 V 加权求和,压成一个向量 (输出向量)。它不产生 token。
    • 真正的 logits(我写成了 logics,正确拼写是 logits)是走完剩下所有层、过 LM head 之后才有的词表分数;对它做 softmax / 采样才选出下一个字。
    • FlashDecoding 全程只动第一层,跟 logits、跟选 token 没有任何关系,logits 从来没被切过。
  • 「npu 里装着权重......一个单元在用 q 和 k 计算 」→ attention 这步不碰权重,用的是 KV cache 里的 K/V;权重用在前面的 QKV 投影和后面的 FFN。(顺带:decode 阶段那些权重计算是"矩阵 × 向量",本身也偏访存瓶颈、并行度不高,所以不只是 attention 闲。)
  • 「其他单元并没有工作 」→ 稍微绝对。实际有 batch × head 数 个块在并行,只是远少于核心数,其余大量空闲。结论不变。
  • 还有:decode 每步只有 1 个 q (当前新 token),不是"每个 token 的 q";打分前要除以 √d,否则 softmax 会退化成"几乎只选一个"。

2.1 attention 内部到底怎么算(我后来复述对的那版)

我当时的说法:

「每个 token 的 q 和前面 token 的 v 相乘打分,然后 softmax,最后 softmax 乘以对应的 V 把前面的向量进行相加最后得到一个新向量」

  • 只错一个字母:「q 和 v 相乘打分」→ 应该是 q 和 k 相乘打分。
  • 后半句「softmax 乘以对应的 V、把前面的向量相加、得到一个新向量」完全正确,这正是 attention 的本质:把全部历史压成一个向量。

精确版:

text 复制代码
sⱼ = q · kⱼ / √d          ① 打分:q 跟每个历史 token 的 K 算相关性,得一个标量
a  = softmax(s)           ② 所有分数一起归一化,得到每个位置的权重
输出向量 = Σⱼ aⱼ · vⱼ     ③ 用权重乘「对应的」V,全部加起来 → 1 个新向量

这个"新向量"是什么 (我专门问过一轮):它是「当前这个字,读完历史之后,被改写成的那一串数字」。不是字、不是下一个 token、不是分数、不是概率、也不是 logits。Transformer 里每个字从头到尾都是一串数字,模型做的全部事情就是一层层改写它;attention 就是其中一道工序。历史 999 个 v 按权重加起来,结果当然只有 1 个向量,就像把 999 份意见平均成 1 份综合意见。

2.2 FlashDecoding 插在哪(我最初的理解)

我当时的说法:

「把 k 分成几个部分,几个 cpu 的单元同时进行 q 和 k 的相关性计算,npu 的几个单元最后把这几块的结果进行拼接」

为什么要这么做:老办法按"要写的字数"分活,decode 时一次只写 1 个字,分不了,于是 1 个核心独自把 999 个字从头读到尾,其余几十个干等。既然 Q 这边切不动,就切"要重看的历史"。

逐条拧:

  • 「把 k 分成几个部分 」→ 是 K 和 V 一起按 token 位置切,不能只切 K,因为每段还要用对应的 V 算加权和。
  • 「几个 cpu 的单元」→ 笔误,是 NPU 计算单元。
  • 「最后把这几块的结果进行拼接 」→ 不是拼接(concat),是加权合并(reduce),这是我当时最纠结的一个点。

2.3 每个核交出什么、怎么合并

每个核只看自己那段,交三样东西:

交什么 是什么 白话
局部输出向量 1 个 128 维向量 「如果世界上只有我这段历史,答案应该是这个向量 」------一份完整的答案草稿
本段最大分数 1 个数 这段里最大的 q·k 分数;算 exp(s − m) 时防溢出,合并时当"统一尺子"的锚点
本段权重和 1 个数 这段所有 exp(s − m) 加起来,即局部 softmax 分母,代表"这份草稿该占多少票"

⚠️ 最要紧的一句 :每份局部输出向量都是和最终答案一样大的完整向量 ,不是"答案的四分之一"。四个核不是各写四分之一份答案,而是各写一份完整的、只是依据不全的答案草稿 。我觉得该"拼接",根子就在把它想成了"答案被切成四块"------其实切的是依据(K/V),不是答案。

合并公式:

text 复制代码
全场最大分数 = max(各段最大分数)                    # 先统一到同一把尺子
票重ᵢ = exp(本段最大分数 − 全场最大分数) × 本段权重和
输出向量 = Σ 票重ᵢ × 局部输出向量ᵢ / Σ 票重ᵢ        # 加权平均成 1 个向量

用今天算过的 4 个 token 例子验过:不切分得到 [1.407, 0.269],切两段再合并也得到 [1.407, 0.269] ------ 不是近似,是逐位等价。

为什么必须带那两个数字:两段票数不一样(有一段含 s=3 的高分 token),直接平均会让"依据弱的一段"和"依据强的一段"平起平坐,结果就错了。

为什么不能是拼接:拼起来维度变 4 倍,后面所有层的矩阵乘都接不上;而且 attention 的本质是"把历史压成一个向量",天然是求和/平均,不是连接。


三、一层其实还没走完:两次残差 + Norm + FFN

我问过「再加一次残差不是已经加过残差了么,把没有进去 attention 之前的向量不是加过了么,为什么又加了一遍 」------不是重复 ,因为一个 Transformer 层里有两个子层 ,每个子层各有一条自己的直通车,而且第二次加的不是同一个向量。

精确写法(Qwen3 这类模型用的 Pre-Norm,归一化在子层之前):

text 复制代码
x0 = 进这一层的原始向量(128 个数)
h1 = Attention( RMSNorm(x0) )        # attention 算出的输出向量
x1 = x0 + h1                        # ← 第 1 次残差:加的是 x0
h2 = FFN( RMSNorm(x1) )             # FFN 算出的改动
x2 = x1 + h2                        # ← 第 2 次残差:加的是 x1,不是 x0!
这一层的输出 = x2

数字上:x0=[1.0,0.5] → h1=[0.2,−0.1] → x1=[1.2,0.4] → h2=[0.3,0.6] → x2=[1.5,1.0]。x1 ≠ x0,所以不是把同一个东西加两遍。

  • 残差的本质 :新 = 旧 + 改动,模型只学"改动量",原始信息始终有条不经过变换的直通车。
  • 为什么要两条 :这一层有两次独立变换(attention 搬历史信息、FFN 加工内容),两次变换自然各配一条。改稿两次的比方:第一次在原稿上批注得稿 1,第二次在稿 1 上批注得稿 2 ------ 第二次保留的是稿 1,但原稿内容也还在,因为稿 1 里含着它。
  • FFN 干什么 :128 → 512 → 128,先撑宽、过一次非线性(SiLU)、再压回来。attention 只负责"把历史信息搬过来",真正加工内容的是 FFN。
  • 顺带记一笔:我某轮把顺序说成"先残差再归一化"是粗糙说法,实际是 Pre-Norm(归一化在子层前,残差加的是未归一化的原始 x)。结论不变。

四、2 ~ 4 层:同样的事重复

我问过「2 到 4 层的每一层 ffn 后还需要残差相加么 」------要,每层都要,而且每层都是两次。层是结构完全相同、权重各自独立的堆叠。

text 复制代码
x = 当前 token 的 embedding 向量

for 第 1 ~ 4 层:
    x = x + Attention_本层( RMSNorm(x) )    # 残差 A
    x = x + FFN_本层( RMSNorm(x) )         # 残差 B

最终状态 = RMSNorm(x)                     # 全模型最后一次归一化
logits = LM_head(最终状态)
  • 每层都从自己的输入 开始算:第 2 层的 x0 就是第 1 层的 x2。
  • 层与层之间没有额外残差,上一层输出直接当下一层输入。
  • 计数:4 层 × 2 次 = 8 次残差相加;归一化是每层 2 次 + 最后 1 次。
  • 每层都用自己的权重重新投影出 q/k/v,新的 k、v 追加写进该层自己的 KV cache,并且每层都要重新读一遍全部历史。所以生成 1 个字 = 读 4 次历史 = 4 次 attention + 4 次 FFN。
  • 全程向量宽度一直是 128,始终不是字。
  • 最后一层 FFN 后的残差也照做,加完才轮到最终 RMSNorm 和 LM head。

这一条正好接回最开始那个问题:每层都重读历史,历史又随生成越来越长,所以 decode 越来越慢;FlashDecoding 优化的就是每层里这 4 次(真实模型 28 次)「读历史 K/V」。


五、最后一层向量 → 词表分数 → 出字

我当时的说法(这版基本全对):

「拿最后一层得到的向量和词表矩阵相乘,词表里有多少个字就会输出多少个字的分数,最后经过 softmax 然后变成字的概率,要么从 top-k 里抽取一个要么直接找分数最高的,然后输出这个分数最高的 token id 或者 top-k 里面其中一个的 token id,根据这个 id 选出来一个字进行输出」

  • 4 层走完、最后一次归一化得到的「最终状态向量」,可以理解为模型把"下一个字该是什么"的全部判断压在这 128 个数里。
  • LM head 是一个 [50000 × 128] 的矩阵,每一行就是词表里某个 token 的模板向量 。相乘的本质是 第 j 个分数 = 最终状态向量 · 第 j 个模板,也就是跟 5 万个候选各比一次相似度 ,一次得到 50000 个 logits。
  • 到这里才第一次出现"跟字有关的东西",但它只是分数,不是概率、更不是字。
  • softmax(logits / temperature) 变成概率(推理时常先 top-p / top-k 过滤),采样或 argmax → 1 个 token id 。这是这一整趟唯一产出的字。
  • Qwen3 里 tie_word_embeddings=true:LM head 和输入 embedding 共用同一份权重,省参数且语义自洽。
  • 两个小修正:严格说是「词表里有多少个 token 就输出多少个分数」,token 不一定是完整汉字(可能是词片段);中文里常一字一 token,但不总是。

六、闭环:id 查 embedding 回到输入

我说错的那句:

「然后继续把最后一层产出的向量作为输入重新进行 decode」

❌ 下一轮输入不是 最后一层那个向量。它是新选出来的 token id 去查 embedding 表,重新得到的一个输入向量。

为什么:最后那个向量是模型对"下一个字该是什么"的判断结果,已经被 LM head 消费掉打分了,是本轮终点产物;下一轮要处理的是「刚生成的这个字本身」,必须把它重新变成模型认识的输入表示。

我后来确认的版本(基本对,只需再精准一点):

「最后一步得到的字,根据这个字查对应的向量,把这个向量作为新一轮 decode 的输入」

→ 模型手里经手的不是「字」这个文本,而是 token id。字只是给外面人看的显示结果。同一个 id 分了两条路:

text 复制代码
采样 → 得到 token id(比如 3842)
        ├─ 查 embedding 表第 3842 行 → 输入向量 → 新一轮 decode
        └─ 把 3842 解码成文本「猫」→ 输出给用户看

所以「查向量」和「输出字」是并行的两条支路,不存在"先有字、再拿字去查表"。

新一轮开始时还顺带两件小事:

  1. 给输入向量加上位置编号(RoPE 在 attention 里用),否则模型不知道这是第几个字;
  2. 把这个新字在每一层 投影出的 k/v 追加进 KV cache,下一轮 attention 才看得见它 ------ 历史又长一个,这正是长文本越生成越慢的原因。

embedding 表是什么(我最后问的那个)

一张 [词表大小 × 向量宽度] 的大矩阵(简化模型 50000 × 128,Qwen3-0.6B 是 151936 × 1024),一行对应一个 token,存着这个 token 的一串数字 。token id 就是行号,直接取那一行,纯查表、不做计算。

为什么非要它:token id 只是整数编号,本身没有语义(3842 和 3843 挨着不代表意思相近),"猫"和"狗"编号可能差十万八千里;换成 embedding 后那串数字带语义,意思相近的字向量也相近,模型算相似度才有意义。

形式 例子 谁在用
字(文本) 「猫」 人
token id(整数) 3842 分词器、采样输出
输入向量(128 个数) [0.12, −0.45, ...] 模型内部

表里的数字是训练时学出来的 (初始随机,随梯度一起更新),不是人工编的字典。另外:位置信息不在这张表里 ,KV cache 里存的也不是它(cache 里是每层投影出的 k/v)。


七、弄错 / 说混的地方 · 完整清单

# 我当时的说法 问题 正确说法
1 prefill「一次性把所有 token 生成 qkv」就完了 漏了 prefill 也做 attention(带因果掩码);QKV 是一次大矩阵乘批量算的,不是逐 token 分开算("可并行"的直觉对)
2 decode「q 和前面每一个 k 和 v 计算得到 logics,softmax 选出 token」 两层 softmax 混成一层 attention 内 softmax 是给历史位置打权重再加权 V 求和,产出 1 个输出向量、不产生 token;logits 是走完剩余层过 LM head 才有的
3 「logics」 拼写 logits
4 「npu 里装着权重......一个单元在用 q 和 k 计算」 混淆 attention 这步不碰权重,用的是 KV cache 的 K/V;权重用在 QKV 投影和 FFN
5 「其他单元并没有工作」 稍绝对 实际有 batch × head 个块并行,只是远少于核心数,其余大量空闲
6 「根据这个 softmax 选出合适的 token」 见 #2 选 token 靠的是最后词表 logits 的 softmax / 采样
7 「把 k 分成几个部分」 漏了 V K 和 V 一起按 token 位置切,每段还要用对应 V 算加权和
8 「几个 cpu 的单元」 笔误 NPU 计算单元
9 「最后把这几块的结果进行拼接」 核心错误 不是 concat,是加权合并(reduce),产出 1 个输出向量、形状不变、数值与不切分等价
10 「每个 token 的 q 和前面 token 的 v 相乘打分」 一个字母 打分是 q·k;V 只在最后被加权
11 「softmax 乘以对应 V 把前面向量相加得到新向量」 ✅ 完全正确 保持,这就是 attention 的本质
12 「O 是什么 / 是数字 0 还是字母 O」 记号 是大写字母 O(Output),不是数字 0;已约定改叫「输出向量」
13 「第一层输出了第一个加权求和后的向量」 措辞 那个向量不是"第一个 token"、也不是任何输出,只是中间状态;prefill 早就产出了第一个字
14 「再加一次残差不是已经加过了么」 误解 两个子层各一条残差,第二次加的是 x1 不是 x0,不是重复
15 「继续把最后一层产出的向量作为输入重新 decode」 闭环错误 下一轮输入是新 token id 查 embedding 得到的输入向量;最后一层向量已被 LM head 消费,用完即止
16 「根据这个字查对应的向量」 细微 经手的是 token id,不是文本;查向量与显示文字是两条并行支路
17 「词表里有多少个字 就输出多少个字的分数」 细微 是 token,不一定等于完整汉字
18 把 FlashDecoding 当成"要开发一个新算法" 任务理解 是适配/搬家:算法在 GPU 上早有了,让它在新硬件上能跑;不是造算子
19 把「四件小事里的第 ① 件找现成工具」理解成从零接 任务理解 现成算子(华为 FIA v2)本来就摆在那儿,先盘弹药再决定写多少代码

另外三个我专门问过、值得记住的区分:

  • head 不是 NPU 计算核心 :head 是模型结构里的"观察角度"(一组独立 Q/K/V 权重 + 一次独立 attention),NPU 核心是硬件;FlashDecoding 是在单个 head 内部把历史切给多个硬件核心并行。
  • 切依据 → 结果加权平均;切视角 → 结果拼接:FlashDecoding 分段合并是前者;16 个 head 的结果合并是后者(真 concat,因为视角互补、平均会互相抵消)。
  • 三个向量别混:输入向量(id 查 embedding 得来,进第 1 层)/最终状态向量(4 层走完,只送 LM head 打分,用完即止)/k、v(每层投影出来,追加进 KV cache)。

八、真实模型只多了什么(单头 → 多头)

Qwen3-0.6B 有 16 个 Q 头、8 个 KV 头 (GQA,两个 Q 头共用一组 K/V,KV cache 省一半)、head_dim=128、hidden_size=1024、28 层、词表 151936。

每层 attention 出口不是一个输出向量,而是 16 份各 128 个数 → 拼接 成 2048 → 用输出投影矩阵混合压回 1024 → 才进入残差 / FFN。除了多这一步,上面第一到第六节全部不变。


九、一段话总结(用我自己的话)

把 prompt 的所有 token 一次性加载进 transformer,一次算出 QKV、把 KV 存进 block,产出第一个字;之后 decode 一个字一个字往外吐,每一轮拿新 token id 查 embedding 表得到输入向量,q 跟前面每一个 k 打分、softmax 出权重、乘对应的 v 把前面的向量相加得到一个新向量(这个向量就是"当前这个字被历史改写了一版",不是字也不是分数),再经过两次残差、归一化、FFN 才算走完一层,这样重复 4 层、每层都重新读一遍历史;最后一层得到的向量和词表矩阵相乘,词表里有多少个 token 就输出多少个分数,softmax 变成概率,要么从 top-k 里抽一个要么直接找分最高的,得到 token id 输出这个字;然后不是把最后一层那个向量送回去,而是拿这个 id 重新查 embedding 表得到输入向量,带上位置编号、把新 k/v 追加进 KV cache,开始新一轮 decode 。而 FlashDecoding 只干一件事:把每层 attention 要读的那串历史 K/V 切成几段、分给几个 NPU 单元同时算,各段交出一份完整的局部输出向量加两个数字,再按统一尺子加权合并(不是拼接)成一个和原来一模一样的输出向量 ------ 长文本因此变快,答案一个字都不改。

相关推荐
jay神25 分钟前
基于 YOLOv8 的手势识别系统(Web 版)
前端·人工智能·python·深度学习·yolo·计算机视觉·计算机毕业设计
武子康4 小时前
H3-World 写了 CPU offload,为什么还不能保证塞进 48GB?
大数据·人工智能·深度学习
严肃小白兔9 小时前
第 04/24 期:深度学习中的损失函数及其数学解释
人工智能·深度学习
BYSJMG9 小时前
计算机毕业设计选题推荐:【基于深度学习的车辆损坏严重程度评估方法研究】ResNet50 图像分类
人工智能·深度学习·目标检测·机器学习·计算机视觉·分类·课程设计
在所不辞兄10 小时前
为什么在电池供电场景下,不能采用固定电压频率策略,而需要动态调整以最大化 Race-to-Sleep 收益?
人工智能·深度学习·神经网络·机器学习
BYSJMG11 小时前
计算机毕业设计选题推荐|【基于深度学习的电梯电动车违规进入检测研究】YOLO26m+可视化大屏
人工智能·深度学习·神经网络·目标检测·机器学习·计算机视觉·课程设计
盼小辉丶11 小时前
PyTorch强化学习实战(35)——多智能体强化学习
人工智能·pytorch·深度学习·强化学习
武子康1 天前
H3-World 的动作能按时发生,为何还不能据此判断实时互动?
人工智能·深度学习·agent
Rocky Ding*1 天前
【三年面试五年模拟】2026-11-05_月之暗面大模型算法岗一面、二面面经全解析
论文阅读·人工智能·深度学习·机器学习·aigc·强化学习·ai-native