0.8MB 跑通 Qwen|第 11-1 篇:大模型推理中多轮对话的浪费——上一轮 prefill 算完就被扔了

简介: 本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测RK3588平台高效运行Qwen3-VL多模态模型;核心突破是默认启用前缀KV复用,使多轮对话prefill耗时骤降27倍,显著消除历史重复计算,大幅提升端侧长会话效率。(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 篇 · 总纲(阿里云社区)

上一篇:10-3《诚实的坑:哪些场景稀疏无收益》 | 下一篇:11-2《前缀缓存键:怎么知道"一样"》

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

一句话导读:多轮对话的浪费:无状态 API 每轮把全历史重发一遍、推理引擎把已算过的 prefill 重新算一遍,越聊越长白算越多——引擎靠什么让第二轮只补算新增的尾巴,从 reset 分支读前缀缓存命中的秘密。

关键词:手搓 Qwen 推理引擎、千问大模型推理、Qwen3-VL、零依赖纯 C、多轮对话、前缀缓存、KV cache、prefill

导语:多轮对话里最贵的浪费:无状态 API 每轮把全历史重发一遍,引擎就把已算过的 prefill 又算一遍,越聊越长、白算越多。本篇从 reset 分支切入看清这笔浪费有多大,并引出默认开启的前缀 KV 复用——同进程两轮实测 prefill 从 46.4s 降到 1.7s。

10 天里我们优化了"单次请求"的每个环节——可真实服务是多轮对话:客户端每轮都把全历史重发一遍,引擎每轮都把历史重新 prefill 一遍。今天先看清这笔浪费有多大,再引出引擎的解法:前缀 KV 复用(默认开)。

1. 知识点:TTFT 里最贵的那段,多轮时被重复支付

一次聊天的第一段耗时(TTFT,首 token 时间)大致由四段组成:

  1. 排队 + 网络(HTTP 请求进来、调度);
  2. 分词(prompt → token id);
  3. prefill(对整段历史做一次前向,把 KV 写进缓存)——10-1 讲过,这是大头:2K 上下文在本板就要 ~46s(见第 3 节实测);
  4. 首词采样。

问题出在"无状态 API"模型上:OpenAI 兼容接口每次请求都要带完整消息历史(messages 数组)。于是第二轮请求的 prefill,会把第一轮已经算过的历史从头再算一遍——第一轮的 KV 明明还躺在内存里,却被当作"新请求"清零重来。

这就是"多轮对话的浪费":越聊越长,每轮白算的 prefill 越多。第二轮白算一轮历史、第三轮白算两轮历史……长会话里,模型大部分时间在重算自己刚算完的东西。

2. 对应代码:浪费发生在 reset,解法是"reset 时保留一段"

先看"每轮请求开头"发生了什么(vllm_server.c 第 405 行起 ist_reset):

static void ist_reset(VLLMServerCtx *ctx, int keep_lcp) {
   
    ...
    int reuse = (ctx->prefix_kv || ctx->disk_kv) &&
                keep_lcp >= PREFIX_KV_MIN_LCP && !g_l3_evict &&
                keep_lcp <= st->cache_len[0];
    if (reuse) {
   
        for (int l = 0; l < nl; l++) st->cache_len[l] = keep_lcp;
        st->seq_len = keep_lcp;
        fprintf(stderr, "[KV-PREFIX] reuse %d-token KV prefix, prefill rest\n",
                keep_lcp);
        ...
        return;               /* 保留前 keep_lcp 个 token 的 KV */
    }
    for (int l = 0; l < nl; l++) st->cache_len[l] = 0;  /* 全清:全部重算 */
    ...
}

看清两件事:

  1. 不开复用时(reuse 为假),请求开始就把 cache_len 全清零——上一轮辛苦写下的 KV 行逻辑上立即作废,新一轮从头 prefill。这是"浪费"的代码级实锤;
  2. 开复用时,引擎允许"保留前 keep_lcp 个 token 的 KV 行",只 prefill 尾巴(第 1321 行 st_qwen_model_prefill_batch(st, ids + keep, n_ids - keep) 只喂了剩余部分)。而 g_prefix_kv 默认就是 1(main.c 第 1649 行,--no-prefix-kv 可关)——默认开,不是需要用户想起来才有的功能。

"怎么知道要保留多长(keep_lcp 从哪来)"是下一篇 11-2 的主角(前缀最长公共子序列 LCP)。本篇只需要接受一个事实:保留多少、清零多少,由"上一轮请求的 token 序列"与"这一轮的"比对决定。

3. 改动后果:同进程两轮,prefill 46.4s → 1.7s

口径:RK3588 / Qwen3-VL-2B / 2026-09 / serve 同进程 /v1/completions 贪婪(前缀复用默认开)。第一轮 prompt = 2044-token 事实长文 + 问题(Alice 的颜色);第二轮 = 第一轮全部 + 模型回答 + 新问题(Bob 住哪),即真实多轮客户端的重发形态。上下文 n 与耗时取引擎日志 [PREFILL-TIMING] 的真实值。

轮次 prefill token 数 prefill 耗时 请求总耗时(含 decode) 模型输出
第一轮(全量) 2044 46.36 s 50.73 s Alice likes the color blue.
第二轮(复用 2034) 31 1.73 s 5.89 s Bob lives in Tokyo.

引擎日志如实记录了命中的那一刻:

[KV-PREFIX] reuse 2034-token KV prefix, prefill rest
[PREFILL-TIMING] n=2044 total=46355.9ms | GEMM=26224.7ms(56.6%) ATTN=18584.2ms(40.1%)
[PREFILL-TIMING] n=31 total=1726.2ms  | GEMM=992.4ms(57.5%) ATTN=716.8ms(41.5%)

判读:

  1. 第二轮只 prefill 了 31 个新 token(问题增量),却是在 2065 token 的完整上下文上采样回答的——正确作答证明保留的 2034 行 KV 真的参与了注意力,不是摆设;
  2. prefill 从 46.36s 降到 1.73s(≈27×);请求总耗时(含每轮固定的 decode 几秒)50.73s→5.89s(≈8.6×)。会话越长、历史占比越高,这个倍数越大;
  3. decode 没有被省:第二轮仍需在完整上下文上逐词生成——复用省的是 prefill,不省 decode(decode 的优化是 Day 10 稀疏与 Day 13 推测解码的主场)。

旁证(仓库文档,x86 基准机 8B/2026-08-28,优化配置与边界说明.md §3.2):前缀复用的进程内验证为 prefill 加速 4.99×(253 token 复用 231 token),且输出与全量逐字节一致。本板这轮 27× 之所以更高,是因为第一轮上下文比例更大(2044 复用 2034)——复用率决定加速比。

4. 学员调试任务

  • A 档(板端动手):按第 3 节口径自己跑一轮"同进程两轮":先发一个长 prompt,再发"长 prompt + 回答 + 追问"。记录两轮日志里的 [PREFILL-TIMING] n= 与 [KV-PREFIX],算你自己板子的 prefill 加速比。把第一轮 prompt 拉长到 4K 再跑一次,观察加速比是否变大(复用率更高)。
  • B 档(纯读源码):读 ist_reset(vllm_server.c 第 405 行起),回答:① reuse 成立需要哪 4 个条件同时满足?② reuse 分支里 cache_len[l] = keep_lcp 与"直接不 reset"有什么不同?(提示:新一轮的尾巴还要继续写 KV,cache_len 必须从 keep 处续写。)

预期输出:你能讲清"多轮浪费 = prefill 被重复支付",并用一组自己的板端数字量化"复用后 prefill 降了 X 倍"。

收尾

  • 本篇源码点名:vllm_server.c(ist_reset 第 405 行起、reuse 门 408–410、[KV-PREFIX] 415、增量 prefill 1321)、main.c(g_prefix_kv=1 默认 1649)。
  • 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
  • 下篇预告:复用保留了 2034 个 token——引擎怎么知道"这两轮的共同前缀恰好是 2034"?11-2 拆最长公共前缀(LCP)与那条"一模一样就不复用"的反直觉安全规则。
相关文章
|
1月前
|
调度 C++
0.8MB 跑通 Qwen|第 7-2 篇:推理引擎的混合精度路由——flags 决定谁用 Q8、谁用 Q4
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型,在RK3588上实现实测。核心创新是混合精度路由:prefill阶段用Q8保精度,decode阶段用Q4省带宽,通过flags比特位动态调度,兼顾速度与质量。(239字)
|
1月前
|
缓存 人工智能 索引
0.8MB 跑通 Qwen|第 12-1 篇:LLM 推理的跨进程恢复——服务重启了,会话不能断
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实现在RK3588上部署Qwen3-VL等多尺寸模型。本文详解`--disk-kv`机制:将KV Cache落盘持久化,支持进程重启后按前缀精准恢复,实现“会话不断连”,2K上下文prefill加速达23.6×,真正打通边缘AI服务可用性最后一环。(239字)
131 1
|
1月前
|
调度 C++
0.8MB 跑通 Qwen|第 13-2 篇:草稿 + 验证——推理引擎里 spec decode 的实现
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测RK3588上运行Qwen3-VL多模态模型;详解speculative decode实现——基于n-gram草稿、批量验证与无损回放,确保输出位级一致,兼顾 correctness 与工程可控性。(239字)
123 1
|
1月前
|
缓存 C语言 C++
0.8MB 跑通 Qwen|第 9-3 篇:推理引擎的 q8 KV 精度对照——量化进注意力,输出差多少
本系列《0.8MB跑通Qwen》手搓零依赖纯C推理引擎,适配Qwen3-VL多尺寸模型,在RK3588上实测q8 KV量化:输出误差仅≈0.01,端到端PPL损失<1%,带宽降为52%,精度与效率达成教科书级平衡。(239字)
160 1
|
1月前
|
缓存
0.8MB 跑通 Qwen|第 8-3 篇:推理引擎的 KV 缓存结构——按 token 还是按头存
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型,在RK3588上实测。本文精析KV缓存的token-major布局设计原理,揭示其如何兼顾prefill与decode访存效率,直击带宽瓶颈。(239字)
109 1
|
1月前
|
调度 C++
0.8MB 跑通 Qwen|第 14-2 篇:单 SSE 流与串行队列——推理引擎的"单机守则"
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多尺寸模型(2B/8B/30B),基于RK3588实测。本文详解“单机守则”:推理状态串行锁定、SSE流独立输出、HTTP与推理线程职责分离,揭示为何默认串行是稳定前提而非性能缺陷。(239字)
108 0
|
1月前
|
调度
0.8MB 跑通 Qwen|第 13-3 篇:推理引擎的回退与收益边界——哪些场景 spec 才划算
本文精析Speculative Decoding在ARM端的收益边界:基于RK3588实测,揭示“62%命中率仍变慢”的根源——单步Decode仅44ms时,验证开销与KV回滚反致负增益;明确开启条件:长上下文、高重复性、大模型、贪婪采样。零依赖纯C,适配Qwen3-VL系列。
112 0
|
1月前
|
NoSQL 调度 C++
0.8MB 跑通 Qwen|第 14-3 篇:推理引擎的批处理正确性边界——margin 0.71 vs 0.032
本篇精读Qwen3-VL系列推理引擎的“位级一致性”本质:批量与串行输出大多逐字节一致,但因浮点累加顺序差异,在near-tie(近平局)场景下存在翻盘风险——margin(如0.71 vs 0.032)决定是否一致。这是工程结果,非数学承诺。
104 0
|
1月前
|
缓存 NoSQL 区块链
0.8MB 跑通 Qwen|第 15-3 篇:推理引擎单步走一次生成——token 循环与 KV 追加
本系列《0.8MB跑通Qwen》用纯C手搓零依赖推理引擎,实测RK3588板端运行Qwen3-VL多模型;本文以gdb单步追踪decode循环,直观展示“采样→前向→KV追加→停判”全过程,将大模型逐token生成钉在胶片上。(239字)
140 0
|
1月前
|
缓存 算法 API
0.8MB 跑通 Qwen|第 11-3 篇:推理引擎里 KV 缓存的一生——内存、容量与淘汰
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎开发,实测RK3588平台流畅运行Qwen3-VL-2B/8B等多版本。本文详解KV缓存“出生—续用/覆盖—死亡”三段式生命周期,揭示预分配、逻辑覆盖、进程级销毁本质,破除“命中即加速、淘汰即释放”认知误区。(239字)
140 0

热门文章

最新文章