全部术语

GLOSSARY ENTRY

Sliding Window Attention 与截断 KV Cache

  • Sliding Window Attention
  • Local Attention
  • Windowed KV Cache

让位置 i 只关注最近 W 个可见 tokens,并在模型语义允许时丢弃更早 K/V,使单层 decode 读取量与缓存容量由上下文长度 T 改为窗口上限 W;它不是 RoPE scaling 或分页管理。

它修改可见性,和“支持更长位置”不是一回事

标准 causal attention 中位置 ii 可见所有 jij\le i。Sliding Window Attention(SWA)再加入距离限制:

Mij={0,max(0,iW+1)ji,otherwiseM_{ij}=\begin{cases} 0,&\max(0,i-W+1)\le j\le i\\ -\infty,&\text{otherwise} \end{cases} oi=softmax(qiKTD+Mi)Vo_i=\operatorname{softmax}\left(\frac{q_iK^\mathsf T}{\sqrt D}+M_i\right)V

WW 是包含当前位置在内的窗口大小。它改变模型的 attention dependency:远于 W1W-1 的 token 不能被该层直接读取。相比之下,RoPE scaling/长上下文位置映射改变 position→angle 映射,不自动限制可见 token;PagedAttention改变 KV 的物理分配/查表,不改变可见性。

Dense band mask 与 decode 截断 cache 的等价路径

T=9,W=4T=9,W=4,每个 Query 可见的 key 数是:

position       0 1 2 3 4 5 6 7 8
visible count  1 2 3 4 4 4 4 4 4

两条路径:

  1. Dense reference:保留完整 K/V:[B,H,T,D],构造 lower-band mask,只在每行最近 WW 列 softmax。
  2. Incremental decode:位置 ii 只提供 K/V[..., max(0,i-W+1):i+1, :],不再需要 mask。

如果 position ids、RoPE 和边界定义一致,两者逐位置输出应相等。窗口 off-by-one 是常见 bug:本页定义 W=4W=4 时当前位置加前三个历史位置,共 4 个。

tensorfull causalwindowed decode
Query[B,Hq,1,D][B,Hq,1,D]
logical K/V source at position i[B,Hkv,i+1,D][B,Hkv,min(i+1,W),D]
score[B,Hq,1,i+1][B,Hq,1,min(i+1,W)]
old KV ownershiprequest/rank 持有全部历史早于窗口且无其他依赖的 blocks 可回收

min(i+1,W) 是逻辑长度;Paged allocator 仍按 block size 向上取整,最后 partial block 和 sink/global blocks 会让物理容量略高。

可运行的逐位置等价实验

import math,platform,torch

torch.manual_seed(211)
DEVICE=torch.device("cuda" if torch.cuda.is_available() else "cpu")
DTYPE=torch.float32

def dense_sliding_window(q,k,v,window):
    tokens=q.shape[-2]
    scores=q@k.transpose(-2,-1)/math.sqrt(q.shape[-1])
    query_pos=torch.arange(tokens,device=q.device)[:,None]
    key_pos=torch.arange(tokens,device=q.device)[None,:]
    visible=(key_pos<=query_pos)&(key_pos>query_pos-window)
    probs=torch.softmax(scores.masked_fill(~visible,-torch.inf),dim=-1)
    return probs@v,visible

def incremental_sliding_window(q,k,v,window):
    outputs=[]; cache_lengths=[]
    for position in range(q.shape[-2]):
        start=max(0,position-window+1)
        k_cache=k[...,start:position+1,:]; v_cache=v[...,start:position+1,:]
        scores=q[...,position:position+1,:]@k_cache.transpose(-2,-1)
        scores=scores/math.sqrt(q.shape[-1])
        outputs.append(torch.softmax(scores,dim=-1)@v_cache)
        cache_lengths.append(k_cache.shape[-2])
    return torch.cat(outputs,dim=-2),cache_lengths

def main():
    b,h,t,d,w=1,2,9,4,4
    q=torch.randn(b,h,t,d,device=DEVICE,dtype=DTYPE)
    k=torch.randn_like(q); v=torch.randn_like(q)
    dense,visible=dense_sliding_window(q,k,v,w)
    incremental,lengths=incremental_sliding_window(q,k,v,w)
    torch.testing.assert_close(incremental,dense,rtol=1e-5,atol=1e-6)
    assert lengths==[1,2,3,4,4,4,4,4,4]
    assert visible.sum(-1).tolist()==lengths
    full_elements=2*b*h*t*d; window_elements=2*b*h*w*d
    print(f"python={platform.python_version()} torch={torch.__version__}")
    print(f"device={DEVICE} dtype={DTYPE} Q=K=V={tuple(q.shape)} window={w}")
    print(f"visible_keys_per_query={visible.sum(-1).tolist()}")
    print(f"incremental_cache_lengths={lengths}")
    print(f"sliding_vs_dense_mask_max_abs_diff={(incremental-dense).abs().max():.3e}")
    print(f"full_KV_elements={full_elements} capped_window_KV_elements={window_elements}")

if __name__=="__main__":
    with torch.inference_mode(): main()

完整文件位于 examples/sliding_window_attention.py。2026-08-13 实际最大差 3.576e-07;完整 K/V 有 144 元素,窗口封顶 K/V 为 64 元素。后者是序列到达 WW 后某一时刻需保留的 payload,不是全序列累计写入量。

教学代码按位置重新 slice 并用 dense matmul,不是 ring-buffer/paged decode kernel,也没有性能意义。生产实现可让逻辑 position 单调增加、物理 slots 按环形或 block eviction 复用;RoPE 必须使用绝对/模型定义 position,而不能因物理 slot 回绕把位置重置为 0。

容量与成本

若全部 LL 层都使用同一窗口,标准 GQA/MHA cache 上限从:

Mfull=2LBTHkvDsM_{full}=2LBTH_{kv}Ds

变为:

Mwindow2LBmin(T,W)HkvDsM_{window}\le2LB\min(T,W)H_{kv}Ds

若只有 local layers 集合 Llocal\mathcal L_{local},global layers 仍随 TT 增长:

M=2BHkvDs(Llocalmin(T,W)+LglobalT)M=2B H_{kv}Ds\left(|\mathcal L_{local}|\min(T,W)+|\mathcal L_{global}|T\right)

每步 local attention 读取/计算也封顶 O(BHqWD)O(BH_qWD),但模型可能通过多层传播间接传递更远信息;直接 receptive field 仍受 architecture 限制。窗口越小,容量/带宽越低,但长距离检索、copy、needle 或跨段一致性可能下降。不能只用 perplexity 或一个随机张量决定业务精度。

何时收益打折或失效

  • runtime 只加 mask,却仍保留/读取完整 KV;计算语义正确但容量收益未兑现;
  • block size 大于窗口余量,internal fragmentation 占比高;
  • global/sink tokens 永久保留,实际 cache 不是纯最近 WW
  • prefix cache 共享 block 仍被其他请求引用,当前请求不能直接释放物理 block;
  • prefill kernel 不支持 band mask fast path,fallback 到 dense math;
  • position ids 随 ring buffer slot 回绕,RoPE 语义错误;
  • 模型训练时没有 windowed attention,却在部署时强行截断。

上下游关系

  • KV Cache定义跨 step 生命周期;本页在模型允许时给它设置每层历史上限。
  • Attention Sink也给 cache 设置上限,但在 recent window 外永久保留少量开头 tokens;它对应 sink ∪ recent mask,不是本页的纯 recent band。
  • PagedAttention负责窗口滑动时 block 分配、驱逐与映射;window mask 不等同 page table。
  • RoPE/长上下文负责位置映射与外推;窗口复用物理 slots 时仍要保留正确 logical position。
  • FlashAttention可以实现 local/band mask 的 tile 跳过与 online softmax,但 backend 支持需实际验证。

参考资料