RoPE 直观理解与示例
1. RoPE 和传统位置编码的区别
传统 Transformer 常见的位置编码方式是把位置向量直接加到 token embedding 上:
token_embedding + position_embedding
RoPE 的做法不同。
token 先得到 embedding,再经过线性变换生成 Q、K、V。RoPE 主要作用在 Q 和 K 上,把每两个维度看成一个二维向量,根据 token 的位置进行旋转。
embedding
生成 Q、K、V
对 Q、K 做 RoPE 旋转
计算 Attention
所以 RoPE 不是给 embedding 乘一个系数,也不是直接给 embedding 加一个位置向量。
2. 一个具体例子
假设 token 序列为:
hello world GPT ASTRA
假设为了演示,每个 token 的 embedding 只有 4 维,并暂时令 Wq 等于单位矩阵,也就是 Q 等于 embedding。
hello = [1, 0, 1, 0]
world = [0, 1, 1, 0]
GPT = [1, 1, 0, 1]
ASTRA = [1, 0, 0, 1]
它们的位置为:
hello m = 0
world m = 1
GPT m = 2
ASTRA m = 3
4 维向量会拆成两组:
维度 (0,1)
维度 (2,3)
RoPE 的基础角频率:
这里 d = 4,因此第 0 组对应 i = 0,第 1 组对应 i = 1:
实际旋转角为:
3. world 的计算
world 的 Q 为:
Q_world = [0, 1, 1, 0]
位置:
m = 1
第一组维度 (0,1)
原向量:
[0, 1]
旋转角:
旋转后:
代入:
第二组维度 (2,3)
原向量:
[1, 0]
旋转角:
旋转后:
最终:
4. GPT 的计算
GPT 的 Q:
Q_GPT = [1, 1, 0, 1]
位置:
m = 2
第一组的旋转角:
所以:
第二组的旋转角:
所以:
最终:
5. 为什么几千维时仍然是每两个维度一组
标准 RoPE 中,Q 和 K 的维度仍然是每两个一组。
例如一个 attention head 的维度是 128:
(0,1)
(2,3)
(4,5)
...
(126,127)
一共 64 组。
每组都有不同的 θ_i,因此不同组随位置变化的速度不同。
6. “旋转快慢”是什么意思
每个 token 最终只根据自己的位置计算一次旋转结果。
所谓旋转快慢,是指:
token 位置 m 每增加 1,
这一组二维向量的旋转角增加多少。
例如:
θ = 1
m = 0: 0 rad
m = 1: 1 rad
m = 2: 2 rad
m = 3: 3 rad
而:
θ = 0.01
m = 0: 0 rad
m = 1: 0.01 rad
m = 2: 0.02 rad
m = 3: 0.03 rad
因此:
θ 大:
位置稍微变化,旋转角就明显变化。
θ 小:
位置要变化很多,旋转角才明显变化。
7. 为什么低索引维度和高索引维度使用不同的 θ
低索引维度的 θ 较大,位置每增加一点,角度就变化较多。
高索引维度的 θ 较小,位置增加很多,角度才逐渐产生明显差异。
可以类比钟表:
低索引维度:秒针
旋转快,对短距离变化敏感。
高索引维度:时针
旋转慢,适合表示更大的位置范围。
因此一个 Q 或 K 向量中,同时存在多种不同的位置尺度。
可以概括为:
低维度相当于秒针,速度快敏感捕捉局部信息,高维度相当于时针,旋转慢,只有长距离才能体现出差别,捕捉远距离信息。同时注意力内积之后还能体现出相对位置。
8. 注意力计算
位置首先独立作用于每个 token 自己的 Q 和 K。
例如:
token A 位于位置 100,token B 位于位置 101。它们的 Q 和 K 分别旋转为:
在计算 Attention 时,A 的 Q 会和 B 的 K 做内积:
旋转矩阵有一个性质,使得最终的位置部分可以转化为两者的角度差。
也就是:
如果位置分别是 100 和 110:
因此 RoPE 虽然先作用在单个 token 的 Q/K 上,但两个 token 做注意力计算时,最终体现出来的是它们之间的相对位置差。
9. 为什么注意力内积能体现相对位置
设两个位置分别为 m 和 n:
做内积:
可以整理为:
因此位置相关部分只依赖:
n - m
而不直接依赖 m 和 n 的绝对值。
这就是 RoPE 能自然把相对位置信息放进 Attention Score 的原因。
10. RoPE 与 KV Cache 的复用
在常见的 RoPE 模型中,KV Cache 通常存的是已旋转的 K和未经过 RoPE 旋转的 V。Q 也会旋转,但不存进 KV Cache。vLLM 的 Llama 实现先对 Q、K 施加 RoPE,再把 Q、K、V 交给 Attention,由后端写入 K、V 缓存。不同架构或特制缓存实现可能采用别的格式。
vLLM 的普通前缀缓存使用父块哈希、当前块的 token IDs 和 extra_keys 计算缓存键,不以 embedding、KV 向量或 RoPE 后的 K 为哈希输入。假设每块 4 个 token,extra_keys 保持一致,原请求的前两个块是:
H0 = hash(NONE_HASH, (101,102,103,104), extra_keys)
H1 = hash(H0, (105,106,107,108), extra_keys)
如果截断前 4 个 token,原来位于第二块的 (105,106,107,108) 变成第一块,其哈希改为:
H0_new = hash(NONE_HASH, (105,106,107,108), extra_keys)
这段 token 没变,父块哈希却从 H0 变成了 NONE_HASH,因此无法命中旧的 H1;即使模型不使用 RoPE,结果也一样。vLLM 前缀缓存设计解释了这条哈希链。没有改变的开头前缀仍可复用,其 token 位置也没有变化。RoPE 不参与普通 APC 的哈希计算或命中判断,它影响的是存入缓存的 K 的数值。
跨位置复用是另一个问题。旧 K 带着旧位置的旋转角,直接放在新位置参与 Attention 会算错相对位置。方案 B 是缓存旋转前的 K,读取时按新位置施加 RoPE;方案 C 是缓存已旋转的 K,读取时校正新旧位置的旋转差。在旋转前的 K 不变的前提下,方案 C 可写为:
当前 LMCache 的 MP CacheBlend 采用方案 C 的缓存方式:保存已旋转的 K,载入后在位置校正内核中先按旧位置逆旋转,再按新位置旋转,结果等价于补上旋转差。方案 B 指缓存时就保存未旋转的 K;这里没有这样存。位置校正只修正旋转;当前文改变使深层 K/V 的内容也发生变化时,CacheBlend 还会选择性重算部分 token。
11. 总结
RoPE 可以理解为给 Q 和 K 中的每两个维度安装一个不同转速的“指针”。
低索引维度:
θ 大
位置变化一点,角度就变化很多
偏向表示短距离差异
高索引维度:
θ 小
位置变化很多,角度才明显变化
偏向表示长距离差异
每个 token 根据自己的绝对位置完成旋转。
随后两个 token 做 QK 内积时,两个绝对旋转角相减,最终 Attention 中体现的是它们之间的相对位置。