0.8MB 跑通 Qwen|第 15-2 篇:推理引擎的 prefill 与 decode——两条路径为何分开

简介: 本篇详解Qwen推理引擎中prefill与decode双路径设计:prefill一次性处理整段prompt(如18 token),批量写入KV缓存;decode逐token循环生成,追加KV。通过RK3588真机gdb断点实证,明确二者独立入口、状态流转与性能动因,手搓零依赖纯C引擎的核心逻辑。(239字)

系列:《0.8MB 跑通 Qwen:从零实现 ARM 零依赖纯 C 推理引擎》(30 天 × 90 篇) | 适配模型:Qwen3-VL-8B-Instruct(千问3_VL_8B_Instruct)· Qwen3-VL-2B-Instruct · Qwen3-30B-A3B | 测试设备:RK3588(4×Cortex-A76 + 4×Cortex-A55,aarch64)

系列总纲:《0.8MB 跑通 Qwen》30 天 90 篇 · 总纲(阿里云社区)

上一篇:15-1《入口架构:参数 → 分发(serve/自检)→ 退出》 | 下一篇:15-3《单步走一次生成》

真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)

一句话导读:prefill 与 decode:推理引擎一次请求先 1 次 prefill、再循环 N 次 decode,两条独立的前向路径为什么必须分开——本篇用 gdb 断点抓到的调用链作证据,看 prompt 整段写 KV、生成逐词补 KV。

关键词:手搓 Qwen 推理引擎、千问大模型推理、prefill、decode、token 循环、GDB、Qwen3-VL、零依赖纯 C

导语:一次请求先来一遍 prefill、再循环 N 次 decode,两条独立的前向路径为什么不能合并?prompt 是现成的、可以批着算,生成的词却因因果依赖只能一个一个来,KV 的生命周期也彼此不同。本篇用 gdb 断点抓到的调用链作证据,看整段写 KV 与逐词补 KV 的分工。

15-1 里我们区分了"教材内核"与"生产路径"。今天把生产路径的两条前向摆开:prefill(一次吃下整段 prompt)与 decode(一次推进一个 token)——serve 每处理一个请求,先走前者、再循环后者。gdb 断点抓到的调用链就是证据。

1. 知识点:一次请求 = 1 次 prefill + N 次 decode

任何请求的"算"都分成两种形态:

prefill decode
输入 整段 prompt(如 18 个 token) 1 个新 token
前向数 1 次(内部按 mini-batch 分批走层) 每 token 1 次
产出 每个位置算出 KV 并缓存;末位 logits 下一步的 logits
时机 请求开头一次 然后循环到 EOS/max_tokens

为什么分开而不是"全按单 token 循环"?三个理由:

  1. prompt 是现成的,可以批:18 个 token 可以 18 路并行走一层、权重只读一遍(Day 14 的"读一遍"红利在 prefill 内部天然存在)——若当成 18 次 decode 串行做,权重要读 18 遍;
  2. decode 的词是现算的,只能串行:第 t+1 个 token 依赖第 t 个的输出(13-1 的因果),必须一次一个;
  3. KV 生命周期不同:prefill 把整段历史写进缓存(只写一次);decode 每步在缓存尾部追加一行,同时要读全历史做注意力(Day 8-10)。

引擎里两者各有独立入口(不是同一个函数加个 flag):prefill 走 st_qwen_model_prefill_batch(vllm_safetensors.c 第 10471 行起),decode 走 st_qwen_model_forward(第 9336 行起)——Day 10 稀疏的 prefill/decode 两套内核、Day 14 的批量前向(st_qwen_model_forward_batch)都是在这两条主干上的变体。

2. 对应代码:serve 层怎么编排这两步

请求处理函数 run_completion(vllm_server.c)先调 prefill(第 1321 行),再进 decode_loop(第 1334 行):

ist_reset(ctx, keep);                                    /* Day 11:保留前缀或清零 */
st_qwen_model_prefill_batch(st, ids + keep, n_ids - keep); /* ① prefill:只算增量 */
...
int rc = decode_loop(ctx, temperature, top_p, min_p, max_tokens,
                     stream, conn, out, met, ...);         /* ② decode:逐词循环 */

decode_loop 的循环体(第 1105–1110 行附近)每轮做:采样 argmax(st->logits) → gen++ → st_qwen_model_forward(st, id)——一次循环一个 token、一次前向(Day 13-1 的"1 循环 = 1 词 = 1 前向"就在这里)。

3. 改动后果:gdb 断点实录——18 个 token 的请求怎么被拆开

口径:RK3588 / 板端 Debug 构建 / 2026-09 / gdb 断在 st_qwen_model_prefill_batch 与 st_qwen_model_forward,客户端发一条 Say hello in one word.。

① prefill 命中(请求开头,缓存为空):

== PF-HIT n_cache0=0 seq_len=0 ==
#0  st_qwen_model_prefill_batch (st=..., token_ids=..., n_tokens=18)
    at vllm_safetensors.c:10471
#1  run_completion (...) at vllm_server.c:1321
#2  handle_chat (...)    at vllm_server.c:2559
#3  server_handler (...) at vllm_server.c:3190
[PREFILL-TIMING] n=18 total=6296.1ms | GEMM=6207.9ms(98.6%) ATTN=23.9ms( 0.4%)

注意传给 prefill 的 prompt 是拼好聊天模板后的完整文本:<|im_start|>user\nSay hello in one word.<|im_end|>\n<|im_start|>assistant\n——18 个 token,一次 prefill 全吃下(Day 21 讲模板注入)。

② decode 命中(prefill 之后,缓存已有 18 行):

== DEC-HIT token_id=9707 seq_len(before)=18 cache0=18 ==
#0  st_qwen_model_forward (st=..., token_id=9707) at vllm_safetensors.c:9336
#1  decode_loop (...)    at vllm_server.c:1109
#2  run_completion (...) at vllm_server.c:1334

判读:

  1. 调用链是分叉的:prefill 的上一帧是 run_completion:1321,decode 的上一帧是 run_completion:1334 → decode_loop:1109——同一请求、两条独立路径,各自的调用栈形状不同;
  2. 状态变化对上了:prefill 进入时 cache0=0, seq_len=0(18 个 token 还没写缓存);decode 进入时 seq_len=18, cache0=18——18 行 KV 正是 prefill 写下的,decode 从第 18 位开始追加;
  3. 一次请求 = 1×prefill + ≤8×decode:max_tokens=8,所以 decode 最多 8 次前向(模型输出 Hello 后提前 EOS 停止,实际更少);
  4. 诚实标注:这是 -O0 Debug 构建,18-token prefill 花了 6.3s(Release 下同样 18 token 是 ~0.15s 量级,Day 3-10 的口径);gdb 实证看的是结构与状态,不是速度。

4. 学员调试任务

  • A 档(板端动手):用 Debug 构建复刻第 3 节(gdb 批模式断 prefill/forward,发一条 2 词以内的请求),抄出 PF 与 DEC 两段 backtrace 与 n_tokens/seq_len,回答:这请求被拆成 1 次 prefill + 几次 decode?为什么不是每次请求都 decode 满 max_tokens?
  • B 档(纯读源码):读 run_completion 的 prefill 调用(vllm_server.c 1321)与 decode_loop 的循环体(1105–1110),回答:① ids + keep 的 keep 从哪来(提示:Day 11)?② decode 循环每轮的"采样→forward"顺序为什么不能反过来?③ prefill 与 decode 各自用哪套注意力内核(Day 10 的两个函数名)?

预期输出:你能用两段 gdb backtrace 讲清"prefill 一次写 18 行 KV、decode 一次补 1 行"的数据流,并指出两条路径的代码入口行号。

收尾

  • 本篇源码点名:vllm_safetensors.c(st_qwen_model_prefill_batch 10471、st_qwen_model_forward 9336、批量前向 9829)、vllm_server.c(run_completion 1321/1334、decode_loop 1105–1110)。
  • 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
  • 下篇预告:路径拆开了,最后一关是"真的走一遍"——15-3 用 gdb 从 prefill 断点一路单步进 decode,逐词看 token 循环与 KV 追加,直到 EOS 停表。
相关文章
|
12天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
7950 15
|
11天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1753 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
11天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
1800 12
|
9天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
25天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
3800 10
|
19天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
2029 1

热门文章

最新文章