
真机实测通过本文实验已在 RK3588 板端实测完成2026-09方法学与原始记录见仓库 docs 与《实验脚本》目录一句话导读sparse_attn_head 的 top-k 块选择拆探针均匀抽样、prefill 重要性预留一半预算、贪心补满与 recency 保险四段代码讲清它如何锁定针、保证逐位确定以及 k1 板端翻车的机制。10-1 画了饼把 KV 切成 32 token 的块只对最该看的 k 块做精确注意力。今天拆实现——sparse_attn_headvllm_safetensors.c 第 8482 行起它凭什么判断哪块该看怎么保证同样输入每次选的块一样把 k 调小到 1 会怎样1. 知识点块选择的三个信号 一个保险该看的块不是猜的引擎用三种信息决定探针probequery 和块内均匀抽样的几个 K算点积取最大值当这块的潜在注意力分。为什么抽样而不是整块全算因为全算就是精确注意力了——探针要的是O(1)/块的廉价筛选。为什么取 max 而不是第一个 token10-1 里那颗needle需要被记住的针可能藏在块中间块头 dull 不代表整块没戏多抽几个位置才不漏针代码注释原话在第 8536–8539 行。prefill 重要性importanceprefill 阶段真实分配了多少注意力给每个 token会按头归约留一份记录imp_head→prefill_importance分配缓冲在第 8289 行。口径注意prefill 用精确注意力时它就是精确的prefill 也开稀疏时它就是稀疏的——记录的和跑的一致。decode 选块时给prefill 真看过的块预留一半预算。为什么探针看的是当前 query 觉得谁重要prefill 记录的是内容本身谁重要——针的注意力质量可能不大softmax 质量摊平但位置是真被读过的。两路信号互补。top-k 兜底剩下的一半预算按探针分从高到低补满。再加一个工程保险最近一块永远保留recency insurance——解码位置附近的 KV 几乎必然被当前词高强度关注直接锁定防止探针抖动把它挤掉。确定性红线同样 query 同样的 KV选的块必须逐位一致否则没法做可复现推理Day 23 起的地基。实现上并列分按块下标决胜不用任何随机数——函数头注释把它记为gumbel_argmax_001红线第 233 行。代价也在这softmax 只在选中的块上重归一化。如果该看的高分块被漏选输出就错——这是 k 太小会翻车第 3 节的根本原因。2. 对应代码逐段导读vllm_safetensors.c 第 8482–8781 行探针循环第 8534–8585 行——每块抽n_probe默认 8个均匀位置if (n_probe 1) n_probe 1; for (int b 0; b n_blocks; b) { /* 每块 */ float best -1e30f; int p0 b * bs; int p_end p0 bs; if (p_end seq_len) p_end seq_len; int step (p_end - p0) / n_probe; if (step 1) step 1; for (int ps p0; ps p_end; ps step) { /* 块内均匀抽 n_probe 个 */ ... dot query · K[ps][kh]; /* NEON vfma 点积 */ if (dot best) best dot; } probe[b] best; /* 块分 抽样中的最大点积 */ if (importance) for (int ps p0; ps p_end; ps) imp_sum[b] importance[ps]; }要点step (p_end-p0)/n_probe保证抽样均匀覆盖整块点积只算 K 的当前头kh128 维成本 ~8×128 MAC/块整段下来 ≈ 一次全量注意力的几十分之一。重要性预选第 8587–8605 行int n_imp 0; if (importance) { int budget (k_blocks 1) / 2; /* 预算一半至少 1 块 */ for (int i 0; i n_blocks n_imp budget; i) { int best -1; for (int j 0; j n_blocks; j) { /* 贪心挑 imp_sum 最大且未用的块 */ if (used[j]) continue; if (best 0 || imp_sum[j] imp_sum[best] || (imp_sum[j] imp_sum[best] j best)) best j; } ... sel[n_imp] best; used[best] 1; } }budget (k1)/2k32 时先锁 16 块给重要性k1 时预算 1——唯一的名额被重要性吃掉探针完全插不上手第 3 节 k1 的翻车与这有关。top-k 补满第 8608–8619 行同样贪心按probe[]从大到小挑probe相等时下标小的赢——这就是确定性的落点。挑不满 k 块时比如块数本来就少自然停止不强行凑数。recency 保险第 8621–8631 行最后一块n_blocks-1没被选上时用它替换掉探针选中的最低分块重要性预留的块不可被挤掉。之后三段是对选中的块做精确注意力逐 token 打分第 8633–8695 行q8 KV 走 int8×int8 点积 scalefp32 KV 走 f32、在选中集上减 max softmax第 8697–8706 行、按权重累加 V第 8708–8763 行——与 9-2 同款内核纪律只是循环范围从[0, seq_len)变成sel[]指向的块。decode 侧调用点第 9176–9191 行把默认参数原样传入prefill_importance是 prefill 结束时按头归约出来的第 11020–11030 行imp_head各头累加到prefill_importance[s]。3. 改动后果把--sparse-k从 32 调到 1板端肉眼可见地翻车口径RK3588 / Qwen3-VL-2B / 2026-09 //v1/chat/completions贪婪 /同一段 2061-token 长上下文事实填充 中段埋针Zaras favorite food is ramen 尾部提问只切稀疏开关与 k。质量判据 针是否被召回needle 一词是否出现在输出里。配置needle 召回板端输出截断decode attn ms/词decode 总耗时 ms/词prefill精确YESZaras favorite food is ramen.9 词49.493.047.5 s稀疏 k32YESZaras favorite food is ramen.9 词与精确逐字节一致44.192.447.7 s稀疏 k1NOThe Leo as is is in Sydney is I the horse Leo is Leo is …40 词复读碎语触 max_tokens 上限仍未收尾12.856.230.0 s判读k32 与精确在输出上逐字节一致——长上下文里针被成功锁定召回 YES这正是文档里稀疏 k32 输出与精确一致口径的板端复现10-1 表 B 的 4K 档还把 decode attn 从 110.7 压到 55.4msk1 最快但输出塌了模型丢掉了几乎全部上下文开始复读附近碎片Leo is … Leo is——肉眼可见的劣化不是数值抖动。它 decode 只要 56.2ms/词attn 12.8ms比 k32 快 39%——省下的不是白拿的是把注意力分布砍到只剩 1 块32 token 视野换来的为什么 k1 必翻车回到第 1、2 节——k1 时唯一名额先被prefill 重要性锁走预算(11)/21重要性最高的是尾部高频被注意的块埋在中间、只被问题真正需要一次的针既挤不进重要性名额又因探针名额为 0 而根本没机会。注意力预算小于注意力分布的真实支撑面必然翻车。诚实标注①最初我们用/v1/completions的裸补全格式Question:…\nAnswer:测同一批场景结果连精确注意力都答不出模型输出no information about Zarra针明明在上下文里——这是 2B instruct 模型对非聊天模板输入的拒答行为不是引擎/稀疏的问题。换成标准聊天模板后精确与 k32 立即逐字节答对。这个坑值得记住测长上下文检索先用 chat 模板确认模型本来就能答再谈引擎优化对质量的影响。诚实标注②质量下边界的完整扫描k16/8/4…本文档在板端只做到 k1 这一个断层点仓库 x86 基准机8B/2026-08-28优化配置与边界说明.md关键词sparse-attn、sparse_attn_head、top-k、长上下文、RK3588上一篇Day 10·1 长上下文的O(n²)困境稀疏注意力降到O(n·k)下一篇Day 10·2 诚实的坑——哪些场景稀疏无收益甚至负优化