0.8MB 跑通 Qwen|第 10-1 篇:大模型推理的长上下文为什么必须稀疏——O(n²) 的注意力 vs O(n·k) 的"只看该看的"

简介: 本系列手搓0.8MB纯C推理引擎,零依赖跑通Qwen3-VL多模型。本文聚焦长上下文瓶颈:RK3588实测显示,注意力耗时占比从18%飙升至71%。提出块稀疏方案——将O(n²)注意力压至O(n·k),实现每词成本与上下文长度无关,为端侧大模型长文本推理提供关键路径。(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 篇 · 总纲(阿里云社区)

上一篇:9-3《q8 KV 精度对照——量化进注意力,输出差多少》 | 下一篇:10-2《sparse top-k 块选择:sparse_attn_head 的实现》

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

一句话导读:大模型推理的长上下文为什么必须稀疏:decode 每词耗时约等于常数 GEMM 加线性增长的注意力,用板端三档实测看注意力占比从约 18% 涨到 71%,讲清块稀疏把每词成本压成与 n 无关的平台。

关键词:手搓 Qwen 推理引擎、千问大模型推理、Qwen3-VL、零依赖纯 C、长上下文、稀疏注意力、sparse-attn、O(n²)、RK3588

导语:长上下文为什么必须稀疏,账要先算清:decode 的 GEMM 是常数、注意力却随上下文线性增长,RK3588 上实测注意力占比从约 18% 涨到 71%。本篇用 O(n²) 与 O(n·k) 的对比,讲清块稀疏如何把每词成本压成与 n 无关的平台,为 Qwen3-VL 这类千问大模型的长文推理铺路。

9-2 说 decode 是"单 q 单遍扫全 KV",9-3 给了它精度背书——可这个"全"字有个代价:上下文每涨一倍,每个新词都要多扫一倍的 KV。今天先不写代码,把账算清楚:长上下文里,为什么全量注意力注定扛不住、而稀疏(sparse)是唯一划算的解。

1. 知识点:decode 的 GEMM 是常数,注意力是线性——占比会一路涨

引擎一次 decode(生成一个词)干两件事:

  1. GEMM 部分(qkv 投影 / O / gate+up / down / lm_head):读一遍权重矩阵,与上下文长度无关。本板实测"每词总耗时 − 注意力"恒为 ~43ms(第 3 节表 B 可复算),即 GEMM 是常数;
  2. 注意力部分:query 要对历史每一个 token 算 Q·K^T、再按权重累加 V(9-2 的"单遍扫全 KV")。历史有 n 个 token,就扫 n 个——随 n 线性增长。

于是每个词的耗时 ≈ C_GEMM + C_attn × n。n 越大,注意力占比越高(下表就是本板实测的 decode 注意力占比,数据见第 3 节表 B):

上下文 n 每词注意力(精确) 每词总耗时(精确) 注意力占比
506 9.7 ms 52.5 ms ~18%(低)
2046 48.9 ms 92.3 ms ~53%(中)
4059 110.7 ms 155.2 ms ~71%(高)

这不是数学题,是内存带宽题:q8 KV 每个 (token, 层) 要 2112 字节(9-1),n=8K 时每层一次全扫就是 ~17MB,28 层 ~480MB(还要按头重复读)。所以同样的模型,短上下文跑得动,长上下文被注意力拖垮——这正是本项目基准报告里 llama.cpp 的实测走势(RK3588 同板):decode 从 1K 档 98.6ms/词涨到 8K 档 411.6ms/词(RK3588_性能基准报告.md)。

稀疏思路:注意力矩阵里绝大多数 token 的权重接近 0——大部分历史对当前词的判断没用。与其扫全量,不如:

  1. 把历史 KV 切成块(block,默认 32 token/块);
  2. 先用极廉价的"探针"找出最该看的前 k 块;
  3. 只对选中的 k 块做全量精确注意力。

这样每词注意力成本从 O(n) 变成 O(n/block + k×block):第一项是找块的探针(每块只算几个点积,很便宜),第二项是与 n 无关的常数(k=32、block=32 → 视野固定在 ~1024 token)。上下文再长,每词注意力都封顶在这个常数里。prefill 侧同理:一批 nb 个 token 对全历史的分数矩阵是 O(nb×n),块稀疏后只看 O(nb×k×block)。

2. 对应代码:稀疏开关藏在哪、什么时候生效

先看全局默认与开关(vllm_safetensors.c 第 235–238 行):

int g_sparse_attn  = 0;   /* 0 = exact attention (default) */
int g_sparse_k     = 32;  /* top KV blocks kept per head ... */
int g_sparse_block = 32;  /* positions per KV block */
int g_sparse_probe = 8;   /* probe samples per block (max-dot fusion) */

默认 OFF——不传参数时引擎跑的是精确注意力(与前 9 天所有实验同口径)。要打开用 --sparse-attn(main.c 第 4872 行起解析 --sparse-attn / --sparse-k / --sparse-block / --sparse-probe)。

真正的门控在两条主路径上,条件一模一样:

decode(vllm_safetensors.c 第 9176 行):

if (g_sparse_attn && !st->use_kv_q4 && c->seq_len > g_sparse_block * 2) {
   
    /* block-probe + top-k 选择 + 有界注意力(sparse_attn_head,10-2 拆) */

prefill(第 10805 行):

if (g_sparse_attn && seq_len > g_sparse_block * 2) {
   
    st_attn_batched_packed_sparse(...);   /* 块稀疏批量注意力 */
} else {
   
    st_attn_batched_packed(...);          /* 精确批量注意力 */
}

两个要点:

  1. seq_len > 2×block(即 >64 token)才进入稀疏。短于 64 token 时没得省,直接走精确路径——这个"边界"是 10-3 的关键材料;
  2. decode 侧多一个 !use_kv_q4:KV 被压成 q4(--kv-q4)时稀疏不可用(10-3 细讲)。

开关、默认参数与门控,都汇总在 优化配置与边界说明.md 第 23、31 行的统一口径里。

3. 改动后果:同一台板子,稀疏开/关的实测差

口径:RK3588(Orange Pi 5 Plus)/ Qwen3-VL-2B-Instruct / 2026-09 / serve 进程内直测 / /v1/completions 贪婪 / VLLM_DEC_PROF=1 引擎逐词计时 / 默认 dual 权重 / 默认 4 线程 / 进程串行、无并发。精确(exact)与稀疏(--sparse-attn --sparse-k 32)用完全相同的 prompt 文本各跑一遍,只有开关不同。上下文 n 取引擎日志 [PREFILL-TIMING] n=... 的真实值。

表 A:prefill(TTFT 的注意力部分)

上下文 n 精确 prefill 稀疏 prefill(k=32) 精确 GEMM/ATTN 稀疏 GEMM/ATTN 总时长变化
506 13.09 s 13.65 s 87.8% / 9.0% 85.8% / 11.1% +4.3%(负优化)
2046 51.84 s 51.50 s 61.2% / 35.9% 61.8% / 34.9% −0.6%(无差别)
4059 148.50 s 106.11 s 39.1% / 58.9% 54.3% / 42.6% −28.6%

表 B:decode(每词中位数;精确= q8 KV 单遍全扫,稀疏= sparse_attn_head)

上下文 n 精确 attn ms/词 稀疏(k32) attn ms/词 精确每词总耗时 稀疏(k32)每词总耗时
506 9.7 13.4 52.5 ms 56.6 ms
2046 48.9 39.1 92.3 ms 82.3 ms
4059 110.7 55.4 155.2 ms 100.0 ms

怎么读这两张表(判读比数字重要):

  1. decode 的 GEMM 真的是常数:把"每词总耗时 − attn"算出来——精确三档分别是 52.5−9.7=42.8ms、92.3−48.9=43.4ms、155.2−110.7=44.5ms,≈43ms 恒定。变的全是注意力;
  2. 精确的注意力随上下文线性涨:n 从 506 涨到 4059(×8),精确 attn 从 9.7ms 涨到 110.7ms(×11)——q8 KV 单遍扫全量(9-2)也救不了"全量"本身;
  3. 稀疏把注意力压成一个平台:k=32 时 n 从 2046 到 4059 只让 attn 从 39.1→55.4ms(涨的是探针扫的块数,不是全量 KV);到 8K/16K 这条曲线会继续摊平(10-3 有边界说明);
  4. 但总时长里 GEMM ~43ms 是地板:n=506 时精确 attn 才 9.7ms、sparse 反而 13.4ms——短上下文开稀疏是负优化(10-3 细讲)。

测量纪律说明:表 B 的"每词中位数"来自引擎 [DEC-SINGLE] 逐词日志(VLLM_DEC_PROF=1);模型生成长度因配置而异(11–32 词不等,提前撞 EOS 即停),中位数不受词数影响。全部为同机串行单轮运行;板子长时间满载有热节流风险(仓库文档已注明可到 ~1.4×),跨时间段绝对值仅供参考,同条件对照结论有效——这也是每张表都用"同一 prompt、只切开关"的原因。

更大规模旁证(仓库文档,配置与平台各异、仅供趋势参考):x86 基准机(Qwen3-VL-8B/16 线程/2026-08-28,优化配置与边界说明.md 第 33–39 行)同条件前后对照:8K prefill 的 ATTN 时间 61.6s→16.0s(-74%),prefill 总时长 232.5s→206.2s(-11.3%,GEMM 占大头所以总时长只省 11%);RK3588 板端基准报告(同一仓库)中 vllm 优化档(sparse k32 + spec 等组合)decode 8K 为 131.9–136.7ms/词,llama.cpp 全量注意力到 8K 涨到 409.4–411.6ms/词——全量注意力的线性成本在长上下文把 decode 拖垮,是两套引擎都验证过的方向。

4. 学员调试任务

  • A 档(板端动手):按第 3 节口径自己复测一组"同一 prompt、精确 vs --sparse-attn --sparse-k 32":短档(n≈0.5K)与长档(n≈4K)各一对。记录每对里 [PREFILL-TIMING] 的 n= 与 [DEC-SINGLE] 的 attn=,回答:n 涨 8 倍时,精确 attn 涨几倍?稀疏 attn 呢?
  • B 档(纯读源码):在 9176/10805 两处门控前各打一个逻辑断点,回答:上下文 32 token 时会不会进稀疏分支?64 token 时?为什么门槛是 2×block 而不是更早?(提示:bs=32 时 64 token 只有 2 块,而 k 默认 32——先算算 n_blocks 与 k 的关系,答案和第 3 节 n=506 那行"想剪枝却剪不掉"是同一个机制。)

预期输出:你能画出一条"精确 attn ~ 线性、稀疏 attn ~ 平台"的曲线,并说清门槛 2×block 的意义。

收尾

  • 本篇源码点名:vllm_safetensors.c(第 235–238 行默认参数、第 9176/10805 行门控)、main.c(第 4872–4882 行 CLI)、优化配置与边界说明.md(第 23、31 行)。
  • 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
  • 下篇预告:"只扫该看的块"说得轻巧——怎么选出"该看的块"才是稀疏注意力的灵魂:10-2 拆 sparse_attn_head,看它怎么用探针、重要性信号和 top-k 把 8K 上下文裁成 32 块。
相关文章
|
1天前
|
调度 C++
0.8MB 跑通 Qwen|第 13-2 篇:草稿 + 验证——推理引擎里 spec decode 的实现
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测RK3588上运行Qwen3-VL多模态模型;详解speculative decode实现——基于n-gram草稿、批量验证与无损回放,确保输出位级一致,兼顾 correctness 与工程可控性。(239字)
|
1天前
|
缓存 C语言 内存技术
0.8MB 跑通 Qwen|第 8-2 篇:在线 softmax——推理引擎的注意力为什么不能先算完 e^x 再除
本系列手搓0.8MB零依赖纯C推理引擎,适配Qwen3-VL多模型,在RK3588(aarch64)真机实测。本文详解softmax溢出陷阱:fp32下exp>88.7即inf,剖析朴素/稳定/在线三版实现,揭示“减max”数学等价性与在线rescale的流式优势。(239字)
|
1天前
|
NoSQL C语言 C++
0.8MB 跑通 Qwen|第 15-1 篇:推理引擎入口架构——参数 → 分发(serve/自检)→ 退出
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测适配Qwen3-VL-2B/8B及30B-A3B模型,在RK3588板端完成真机验证。本文详解`main()`入口如何通过命令行参数分发至serve、离线转换或自检三种命运,并用GDB抓取真实调用链,厘清初始化与退出逻辑。(239字)
|
1天前
|
缓存 算法 API
0.8MB 跑通 Qwen|第 11-3 篇:推理引擎里 KV 缓存的一生——内存、容量与淘汰
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎开发,实测RK3588平台流畅运行Qwen3-VL-2B/8B等多版本。本文详解KV缓存“出生—续用/覆盖—死亡”三段式生命周期,揭示预分配、逻辑覆盖、进程级销毁本质,破除“命中即加速、淘汰即释放”认知误区。(239字)
|
1天前
|
人工智能 开发者
阿里云Token Plan个人版:四个版本区别对比、费用价格、Credits计费用量及问题解答FAQ
阿里云Token Plan个人版含Lite/ Essential/Standard/Pro四档:Lite(¥39,1.15万Credits)适合轻量使用;Essential(¥79)用量翻倍;Standard(¥139,推荐)起赠Harness权益;Pro(¥499)适配高并发与海量调用。权益、额度、并发数逐级提升,详见官网。
|
1天前
|
人工智能 运维 前端开发
接入多家模型供应商之后,我总结的 7 条铁律
AI 应用接了第二家模型供应商之后,事情就不再是"多加一个接口"那么简单——不是调不通,是钱会算错。这篇讲我们定下来的 7 条规则:幂等键、任务落库时机、备用线路的切换边界、退款、凭据隔离、付款来源固化、密钥不进浏览器。
|
1天前
|
存储 弹性计算 JSON
把 HelloAGENTS 的项目知识库放进云上开发环境
HelloAGENTS 云上部署指南:明确区分长期知识(进 Git/共享存储)与短时运行时(本地、自动过期),指导 ECS 多机持久化、同步边界、快照策略及安全升级回滚,助团队高效复用项目知识。
27 1
|
2天前
|
JSON Java Linux
0.8MB 跑通 Qwen|第 3-3 篇:推理引擎的零依赖自研 util——"不引第三方也能活"的边界感
本文实测验证“零第三方依赖”的务实边界:OS已支持的(如Linux原生UTF-8路径)仅薄封装为宏;OS缺失且轻量关键的功能(FNV-1a校验、小端memcpy、平台工具宏)才自研。RK3588真机验证(2026-09),代码简洁、可移植、无冗余。
0.8MB 跑通 Qwen|第 3-3 篇:推理引擎的零依赖自研 util——"不引第三方也能活"的边界感
|
3天前
|
移动开发 编译器 Linux
0.8MB 跑通 Qwen|第 2-2 篇:推理引擎的平台层——一个头文件守住全部平台契约(vllm_platform.h)
本文实测于RK3588(2026-09),提出“平台层契约”设计:将NEON、mmap、绑核等平台依赖统一收口至`vllm_platform.h`,通过`ST_HAVE_NEON`等宏提供唯一真相,配合`#error`门闩实现错误前移——有守卫仅报1行错,无守卫则引发46处误导性编译失败。
0.8MB 跑通 Qwen|第 2-2 篇:推理引擎的平台层——一个头文件守住全部平台契约(vllm_platform.h)
|
1天前
|
机器学习/深度学习 存储 弹性计算
阿里云服务器ECS实例架构:X86计算和Arm计算有什么区别?
阿里云ECS架构含X86与Arm两大主流类型:X86(Intel/AMD/海光)vCPU基于超线程,兼容性强,适用于传统企业应用;Arm(倚天710/Ampere)vCPU为物理核心、资源独享,能效比高,适合容器、微服务及CPU型AI场景。(239字)
24 0

热门文章

最新文章