系列:《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 篇 · 总纲(阿里云社区)
上一篇:13-3《回退与收益边界:哪些场景 spec 才划算》 | 下一篇:14-2《单 SSE 流与串行队列:引擎的"单机守则"》
真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)
一句话导读:连续批处理:真实服务是一堆请求同时进来,naive 排队串行会让第 8 个人等前面 7 个全部生成完——本篇讲推理引擎 iteration-level 按步合批的调度思想,把多个请求的 decode 步合并成一次前向。
关键词:手搓 Qwen 推理引擎、千问大模型推理、连续批处理、iteration-level、并发请求、Qwen3-VL、零依赖纯 C
导语:真实服务里请求是一批批同时来的,谁先谁后?naive 串行会让排在后面的人一直干等。本篇讲推理引擎的 iteration-level 调度思想:每一轮把想要下一步的请求凑成一个动态集合,共用一次权重读取,让「读一遍权重」同时服务多个人。
前 13 天都在优化"一个请求"。可真实服务是一堆请求同时进来:8 个用户各聊各的,谁先谁后?naive 做法是排队串行——第 8 个人要等前面 7 个全部生成完。Day 14 的三篇讲引擎的批处理:把多个请求的 decode 步合并成一次前向,让"读一遍权重"同时服务多个人。
1. 知识点:为什么 decode 可以"合批",而 naive 串行是浪费
decode 每步都在做同一件事:读一遍全部权重,给"当前 token"算下一步。第 13-1 说这是带宽瓶颈——但瓶颈恰恰意味着可以共享:
- 8 个请求各自 decode:8 次前向 = 8 遍权重读取;
- 把这 8 个请求的"当前步"拼成一个 mini-batch 做一次前向:1 遍权重读取,同时算出 8 个下一步。
只要 batch 内各请求的序列长度对齐(KV 各自独立、按请求索引),就能合并。这就是 iteration-level(按"步"而非按"请求")调度的核心:每一轮迭代,把所有想要下一步的请求凑齐,统一走一次前向。
两个关键工程点:
- 请求可以中途加入/离开:有人 3 轮就结束、有人要 30 轮——batch 不是固定 8 人小组,而是每轮动态集合(这区别于静态"一次处理一组请求"的 naive batching);
- 只在前向处合流:每个请求的 token 采样、SSE 流式输出仍是独立的——合批的是"算",不是"状态"。
2. 对应代码:引擎线程 + 每请求 worker 线程
线程模型写在 vllm_batch.c 文件头注释里(第 1–14 行),是全文最好的一张图:
/* Engine thread: the only caller of st_qwen_model_forward_batch. It polls
* the node table for want_step nodes, runs ONE forward for the whole set
* (weights read once per step), then bumps each node's done_epoch.
*
* Worker thread (per HTTP request): acquires a node (inference state),
* prefills under the engine mutex, then per step: sample -> submit token ->
* wait for done_epoch -> stream the decoded chunk. Requests join/leave the
* batch freely; finished requests free their state back to the pool. */
两句话拆开:
- 引擎线程(唯一调
st_qwen_model_forward_batch的线程,第 218 行):轮询"哪些请求想要下一步"(want_step),凑成一个集合,跑一次前向(权重只读一遍),然后统一"发工资"(bump done_epoch)——每个等待的 worker 线程据此醒来继续; - 每请求一个 worker 线程:在引擎 mutex 下做 prefill,然后进入循环:采样自己的下一步 token → 提交进 batch → 等引擎线程的那次前向跑完(等 done_epoch)→ 把自己的 chunk 流式发给客户端。
这就是"批处理与 HTTP 协作"的答案(14-2 的承接):HTTP worker 只负责自己的请求,引擎线程独占推理前向,二者通过 want_step/done_epoch 握手,请求随时 join/leave(第 13 行原话),VB_MAX_BATCH = 32(第 29 行)封顶。
开关:--batch-max N(main.c 第 4935 行,默认 0=关)。引擎一次 decode 步现在要"喂多个 token"——对应的内核是 st_qwen_model_forward_batch,它在单 token 前向之外存在(第 9829 行起有批量 GQA/归一化 worker 的结构)。
3. 改动后果:8 个并发请求,批量 vs 串行(板端实测)
口径:RK3588 / Qwen3-VL-2B / 2026-09 / serve 贪婪 /
--batch-max 8。8 个短问答请求(内容互不相同),先串行发(队列深度 1,batch 不生效),再8 线程同时发(batch 生效),同一服务进程、同一批 prompt。
| 模式 | 8 请求总耗时 | 备注 |
|---|---|---|
串行(--batch-max 8 服务,队列深度 1) |
12.95 s | batch 空转:每请求独立 decode,权重读 8 遍 |
8 并发(--batch-max 8) |
20.93 s | 引擎线程按步合批,但这批短问答反而更慢(见判读 2) |
| 对照:8 并发(无 batch 服务) | 10.48 s(≈同服务串行 11.35 s) | 推理状态锁上排队:并发≈串行 |
判读:
- 输出全部逐字节一致(Blue / Tokyo / tea / Green / Paris / coffee / Purple / Berlin 各模式相同)——批量合批没有改变结果(14-3 讲 near-tie 边界);
- 这批短问答开 batch 反而更慢(20.93s vs 12.95s,+62%):请求各只有 2–6 个 token,合批的"权重只读一遍"红利没机会累积,反而让每轮都迁就批内最慢者(8 个请求同步进退、约 20s 一起收尾)。batch 的收益条件是 decode 步数足够多——仓库文档 优化配置与边界说明.md §3.6 的 1.7×(8B 长对话)与本节 2B 短问答的"+62%"都是真的,差别在场景;短问答要量过再开;
- 引擎日志的
batch_active(vllm_admin.c 第 389 行,批内请求数)在并发期间 >1——/admin/api/status可抓活证据。
测量纪律:同批 8 个 chat 问答,串行基线在不同进程间测得 11.35–12.95 s(±~7% 抖动,Day 7 的老朋友);本文主对照 12.95 vs 20.93 是同服务进程内先后测得,结论方向不受抖动影响。
4. 学员调试任务
- A 档(板端动手):复测第 3 节:
--batch-max 8起服务,用并发客户端发 8 个请求,再串行发同一批,记录两组总耗时与各自的输出;顺手curl /admin/api/status看并发期间的batch_active。 - B 档(纯读源码):读 vllm_batch.c 头注释(第 1–14 行)与引擎线程轮询(第 218 行附近),回答:① 为什么"权重只读一遍"在 decode 场景是收益而非负担?② 请求中途结束,引擎线程怎么知道这一轮不用等它?③
VB_MAX_BATCH=32超了会怎样(提示:VB_SLOT_WAIT_MS 120s 的等待)?
预期输出:你能画出"引擎线程 ↔ N 个 worker 线程"的握手图(want_step → 一次 forward → done_epoch),并解释 batch 为什么是"动态集合"而非"固定小组"。
收尾
- 本篇源码点名:vllm_batch.c(线程模型注释 1–14、引擎线程 218、
VB_MAX_BATCH29)、main.c(--batch-max4935)、vllm_admin.c(batch_active389)、vllm_safetensors.c(批量前向 9829+)。 - 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
- 下篇预告:批处理线程有了,可每个请求的 HTTP 流还是独立的——14-2 讲 serve 的"单机守则":为什么引擎坚持单 SSE 流 + 串行队列,批处理线程和 HTTP worker 并发乱配会发生什么。