11. Paged attention 与 online softmax
11.1 块表:把逻辑位置翻译成物理地址
把序列按 \(B\) 个 token 一块切开。逻辑位置 \(p\)(从 0 开始)落在
\[ \ell_b=\left\lfloor\frac{p}{B}\right\rfloor,\qquad \text{slot}=p\bmod B . \]第 \(\ell\) 层第 \(\ell_b\) 个逻辑块对应的物理块号存在 block table 里:\(\text{phys}=\text{table}[\ell][\ell_b]\)。物理块在池子里,池子按 \(B\times \text{kvDim}\) 连续排列,于是这个位置的 K 向量起始偏移是
\[ \text{offset}=\text{phys}\cdot B\cdot \text{kvDim}+\text{slot}\cdot\text{kvDim}. \]再乘上 4 字节(float32)就是字节地址。逻辑上连续的序列,物理上可以散落在任意块里。
一个长度 \(T\) 的序列每层需要 \(\lceil T/B\rceil\) 个块,所有层共 \(L\lceil T/B\rceil\) 个块。池子总字节:
\[ \text{numBlocks}\times B\times 2\times \text{kvDim}\times4 . \]例:\(B=16\)、\(T=100\)、\(L=28\)、kvDim=1024。每层 7 块,共 196 块;每块 \(16\times2\times1024\times4=131{,}072\) B=128 KiB;合计约 24.5 MiB。最后一个块只用了 4 个 slot,内部碎片最多 \(B-1\) 个 token/层。
11.2 为什么需要 online softmax
标准注意力要先看到全部分数才能算 \(\max\) 和 \(\sum e^{s_j-m}\)。但分页后 K/V 分散在多个块里,如果先把所有块 gather 成连续数组,就退回了 \(O(T)\) 的复制(stage A 的做法)。stage B 想“边读块边算”,于是需要一个可以分块合并的 softmax。
设全局最大值 \(m\),未归一化累加器
\[ l=\sum_j e^{s_j-m},\qquad o=\sum_j e^{s_j-m}v_j, \]最终输出是 \(o/l\)。对一个新块 \(b\),块内最大值 \(m_b\),块内统计
\[ l_b=\sum_{j\in b}e^{s_j-m_b},\qquad o_b=\sum_{j\in b}e^{s_j-m_b}v_j . \]合并时取 \(m_{\text{new}}=\max(m,m_b)\),把旧状态和新块都缩放到同一个基准:
\[ \boxed{ \begin{aligned} o_{\text{new}}&=o\,e^{m-m_{\text{new}}}+o_b\,e^{m_b-m_{\text{new}}},\\ l_{\text{new}}&=l\,e^{m-m_{\text{new}}}+l_b\,e^{m_b-m_{\text{new}}}. \end{aligned}} \]为什么对?把右边展开,\(o_{\text{new}}\) 的每一项都是 \(e^{s_j-m_{\text{new}}}v_j\):旧项乘 \(e^{m-m_{\text{new}}}\) 后是 \(e^{s_j-m}e^{m-m_{\text{new}}}=e^{s_j-m_{\text{new}}}\),新块同理。所以合并后正好是“以 \(m_{\text{new}}\) 为基准的全局累加器”。最终 \(o/l\) 与一次性 softmax 完全相等(只差浮点舍入)。
数值稳定:两个指数都 \(\le0\),不会溢出;第一个块之前 \(m=-\infty\),\(e^{-\infty}=0\),旧状态自然清零。
softmax 可以“分块做、再合并”,因为指数乘法可以拆成“先按块内最大值缩放,再乘回全局修正因子”。这就是 flash attention / paged attention 的数学核心。
11.3 块共享与写时复制
如果两个序列有相同前缀,它们的 block table 可以指向同一批物理块,节省内存。代价是:共享块必须只读。当某个序列要往共享块里写新 token 时,先分配一个新块、把旧内容复制过去(copy-on-write),再改新块。
用引用计数管理:refs[p] 表示有多少个 block table / 前缀缓存指向物理块 \(p\)。分配时 refs=1,共享时 refs++,释放时 refs--;只有减到 0 才真正回到空闲栈。这样“释放一个仍被共享的块”只是撤掉自己那一份引用,不会破坏别人的数据。
对应代码:model/pagedattn.go 的 attentionPaged(合并公式在 mNew / corrOld / corrB 那几行),model/pagedkv.go 的 BlockManager / PagedKV。