Qwen3-ASR 课程 · 第 8 课 · 路线第 5 站 · 推理篇(上)

推理全过程 —— 一段音频到一行文字的完整旅程

⏱ 约 30 分钟 · 前置:向量流水线 · 本课目标:跟着一段 10 秒音频走完引擎内部的全部分工位——模板组装 → Prefill → Decode → Detokenize,补上「KV Cache」这个此前一直缺席的关键角色

第 7 课你在纸上算过每一站的矩阵;这一课把它们串成一次真实的推理调用,并补讲此前刻意跳过的两个引擎机制:KV Cache 和 Prefill/Decode 两阶段。理解了它们,你就理解了压测报告里 TTFT 为什么随并发上升、流式为什么会积压。

出发:一段 10 秒的音频

例子贯穿全课:10 秒录音,内容是「明天下午三点开会」。按已学知识,进引擎前它已经变成:

接下来的问题是:这 124 个向量怎么「变成」文字?答案分四站。

第 1 站:输入组装(模板即序列)

第 6 课的对话模板,现在从张量视角看——它就是最终送入 LLM 的位置序列:

位置内容嵌入来源
1–6<|im_start|>system … 热词(如有)…<|im_end|>词嵌入表(第 7 课第 5 站:查表 = one-hot 左乘)
7–9<|im_start|>user<|audio_start|>词嵌入表
10–133<|audio_pad|> × 124音频嵌入(投影器输出)
134–136<|audio_end|><|im_end|><|im_start|>assistant词嵌入表
137–142language Chinese <asr_text>词嵌入表(输出前缀,强制语言)

合计约 142 个位置。注意:序列里没有「明天下午三点开会」——那是模型要生成的,不是输入。

第 2 站:Prefill(预填充)—— 一次算完全部输入

Prefill 把这 142 个位置一次性并行过一遍 28 层 Transformer:

  1. 每层:142 个位置各自算 Q/K/V,做自注意力(第 2 课),过 FFN;
  2. 关键副产物:每层每个位置的 K、V 向量被缓存下来——这就是 KV Cache;
  3. 读出最后一个位置的隐藏态 → 输出层(词表 151,936)→ logits → softmax → 本步预测:「language」。
KV Cache 为什么是必需品 生成「天」的时候,模型要「看」前面所有位置(音频 124 个 + 已生成的字)。若每步都重算全部历史的 K/V,成本随长度平方爆炸。KV Cache 把历史 K/V 存起来,每步只算新增的那一个位置——用显存换时间,这是所有 Transformer 推理的基石。

Prefill 的算力特征:大量并行矩阵乘——142 个位置同时算,GPU 的并行能力正好吃满。它是「算力 bound」的活儿。

第 3 站:Decode(逐词元解码)—— 一步一个字

从「language」开始,进入循环:每步把上一步的输出 token 嵌入送入模型,只计算一个新的位置,注意力通过 KV Cache 读取全部历史:

步已生成本步输出
1(prefill 后)language
2languageChinese
3language Chinese<asr_text>
4…<asr_text>明
5–10…明天 → 下 → 午 → 三 → 点 → 开
11…开会会
12…开会<|im_end|>(停止)

Decode 的算力特征与 Prefill 完全相反:每步只算 1 个位置(并行度=1),但每一步都要把全部权重从显存读一遍。算一下账:1.8GB 权重 ÷ T4 的 320GB/s 带宽 ≈ 每步 5.6ms 的纯读取下限——这就是「访存 bound」:瓶颈不在算,而在搬。

这一站解释了三件你已经见过的事 ① 压测报告里 TTFT 随并发暴涨:prefill(算力)在排队;② GQA 有意义:KV 少一半,访存少一半;③ 批处理有魔力:8 路流同时 decode,权重只读一次、八家分摊——RTF 随并发涨得慢的根源(Table 2)。

第 4 站:Detokenize(后处理)—— 从词元到正文

解码出的是词元 id 序列,后处理三步:① id → 词表文本(词嵌入表的反查,不是矩阵乘,是查字典);② 剥离特殊词元(language、Chinese、<asr_text>、<|im_end|>);③ 取 <asr_text> 之后的正文 → 「明天下午三点开会」。processor.batch_decode(..., skip_special_tokens=True) 一步做完。

推理账本:一段音频的全部开销(可运行)

把四站的账算成数字(0.6B、T4 fp16、10 秒音频;理论下限,真实值含框架开销):

# 推理账本:0.6B @ T4 fp16,10 秒音频(参数来自 config.json / T4 规格) P_FLOP = 0.9e9 # 总参数 ~0.9B -> 每次"全权重前向" ≈ 2*P FLOPs BW = 320e9 # T4 显存带宽 320 GB/s TFLOPS = 65e12 # T4 FP16 算力 W_BYTES = 1.8e9 # fp16 权重 1.8 GB audio_s = 10 # 改我:音频时长 embeds = round(12.5 * audio_s) # 音频嵌入(12.5/秒,第 6 课) prompt = embeds + 30 # + 模板/热词/语言前缀 steps = 12 # 输出词元数(含特殊词元) kv_end_mb = (prompt + steps) * 112 / 1024 # KV/token = 112KB(第 7 课) prefill_ms = 2 * P_FLOP * prompt / TFLOPS * 1000 # prefill:纯算力 step_ms = W_BYTES / BW * 1000 # decode 每步:读全部权重(访存) decode_ms = steps * step_ms print(f"输入序列 {prompt} 个位置(音频 {embeds} + 模板/前缀 {prompt-embeds})") print(f"Prefill:FLOPs≈{2*P_FLOP*prompt:.1e},纯算力 ≈ {prefill_ms:.1f} ms") print(f"Decode:{steps} 步 x 每步 {step_ms:.1f}ms(权重全量读取)= {decode_ms:.0f} ms") print(f"KV Cache 终态 ≈ {kv_end_mb:.0f} MB(池 11GB 的零头)") print(f"TTFA 理论下限 ≈ prefill + 首步 decode ≈ {prefill_ms + step_ms:.0f} ms") print("对照:官方报告 TTFT 92ms@并发1(2 分钟音频/更快的 GPU)——量级一致")
口径提醒 上面的数字是纯算力/带宽的理论下限:不含框架开销、注意力计算、排队。官方报告 0.6B 的 TTFT 是 92ms(且音频长达 2 分钟)——你测出来的 T4 数字会比理论下限大几倍,差距就是"工程税"。压测课(0011)量的正是真实值。

检索练习

本课主读材料

去读(约 20 分钟,配代码)

Karpathy: Let's build GPT — the coding part(Zero to Hero 系列后半):视频里 idx_cond = idx[:, -block_size:] 那一行,就是"decode 只看最后 block_size 个位置"的代码形态;KV Cache 是它的工程化改造。

进阶(可选):vLLM 文档的 PagedAttention 概念页——KV Cache 的"操作系统级"管理,生产推理引擎的核心。

本课新术语已入 术语表:KV Cache、Prefill、Decode、Detokenize。

我是你的老师,别客气 账本里任何一步和你的直觉冲突(比如"为什么 decode 不重算注意力"),把疑问原样贴回来。下一课讲流式——你会看到 KV Cache 与"不封死块"如何配合,让字一边被说出一边被写对。