
通俗理解RoPE、2D-RoPE、M-RoPE 原创
本文通过将这些方法可视化呈现为旋转操作和维度拆分,能让旋转位置编码(RoPE)、二维旋转位置编码(2D-RoPE)以及多模态旋转位置编码(M-RoPE)的核心概念更直观、更易于理解。
为什么需要位置嵌入?
假设有两个语言模型:一个一次只能处理一个词,另一个则可以并行处理所有词。
现在,有一个词序列,比如“Dog eats food”。
- 对于第一个模型,输入的顺序很重要,因为它必须先处理“Dog”,再处理“eats”,最后处理“food”。但显然,这样既缓慢又低效!
- 对于第二个模型,输入的顺序不重要,因此可以一次性输入所有词,甚至是乱序的,比如“food”、“Dog”、“eats”。由于这个模型可以并行处理所有词,所以速度快得多。
第二个模型的问题在于它不知道词的顺序。因此,需要向输入嵌入中添加一些位置信息。
现在,想象一下,用N个嵌入向量代替词,每个向量的维度为n_dim
。举个例子:4个嵌入向量,每个的维度n_dim=8
,且初始化为1:
现在将位置嵌入0、1、2、3分别应用到每个嵌入向量上。还使用了一种假想的方法,即简单地将位置索引加到每个嵌入向量上。因此,第一个嵌入向量就会是1+0,第二个是1+1,以此类推。
当然,这只是一个用于说明该概念的简单例子。实际上,这种方法是行不通的。那么,该怎么做呢?
RoPE
RoPE是旋转位置嵌入(Rotary Position Embedding)的缩写。目前广泛运用在LLM中,其是一种在Transformer模型的输入嵌入中编码位置信息的方法。
RoPE的工作原理很简单,就是在二维空间中旋转输入嵌入向量。这里不深入数学细节,但举一个简单的例子:
输入向量为[x=0, y=1]
,每个位置会将该嵌入向量逆时针旋转20度()。
在现实世界中,并非只有2个维度,而是有n_dim个维度。例如,n_dim = 4:
那么,如何在n维空间中旋转一个向量呢?答案很简单:将这个向量拆分成多个二维对。
为了简化说明,会用箭头来替代每一对(二维对):
现在有趣的部分来了:不会用相同的角度旋转每个向量,因为这样很快就会耗尽所有可能的角度。
理解这一点的最佳方式就像看待一个时钟🕓:秒针每转一整圈,分针只转动一小部分。
在RoPE中,旋转的量被称为频率(记为f),其定义为:
为简单起见,假设第一对的频率f仍为20°,第二对的频率f为10°:
如所见,第一对的旋转速度比第二对快,就像时钟的秒针比分针转得快一样。
就像时钟的三根指针可以用来表示一天中的86400秒那样,可以用同样的思路在RoPE中表示所有的n维维度。
gemma的RoPE实现如下:
def _compute_default_rope_parameters(
config: Optional[PretrainedConfig] = None,
device: Optional["torch.device"] = None,
seq_len: Optional[int] = None,
**rope_kwargs,
) -> tuple["torch.Tensor", float]:
"""
Computes the inverse frequencies according to the original RoPE implementation
Args:
config ([`~transformers.PretrainedConfig`]):
The model configuration.
device (`torch.device`):
The device to use for initialization of the inverse frequencies.
seq_len (`int`, *optional*):
The current sequence length. Unused for this type of RoPE.
rope_kwargs (`Dict`, *optional*):
BC compatibility with the previous RoPE class instantiation, will be removed in v4.45.
Returns:
Tuple of (`torch.Tensor`, `float`), containing the inverse frequencies for the RoPE embeddings and the
post-processing scaling factor applied to the computed cos/sin (unused in this type of RoPE).
"""
base = rope_kwargs["base"]
dim = rope_kwargs["dim"]
attention_factor = 1.0 # Unused in this type of RoPE
# Compute the inverse frequencies
# 计算RoPE公式40中的theta,每2个维度共用一个inv_freq
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float) / dim))
return inv_freq, attention_factor
class GemmaRotaryEmbedding(nn.Module):
def __init__(self, config: GemmaConfig, device=None):
super().__init__()
self.rope_type = "default"
self.max_seq_len_cached = config.max_position_embeddings
self.original_max_seq_len = config.max_position_embeddings
self.config = config
# 计算前面一半维度旋转角度theta,用于和position_ids相乘
inv_freq, self.attention_scaling = _compute_default_rope_parameters(self.config, device)
self.register_buffer("inv_freq", inv_freq, persistent=False)
self.original_inv_freq = self.inv_freq
def forward(self, x, position_ids):
inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device)
position_ids_expanded = position_ids[:, None, :].float()
device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps"else"cpu"
with torch.autocast(device_type=device_type, enabled=False): # Force float32
#这里的freqs只是前面一半维度的
freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2)
# 在hidde_dim维度拼接起来之后freqs最后一维的维度才等于hidden_size
emb = torch.cat((freqs, freqs), dim=-1)
# 计算 cos(m*theta)和sin(m*theta)
cos = emb.cos()
sin = emb.sin()
return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
def rotate_half(x):
"""
Rotates half the hidden dims of the input.
将输入x后hidden_dim的半部分取反并拼接到原来前半部分的前面
"""
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)
def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
"""
Applies Rotary Position Embedding to the query and key tensors.
将q,k乘以得到的旋转矩阵cos,sin
"""
cos = cos.unsqueeze(unsqueeze_dim)
sin = sin.unsqueeze(unsqueeze_dim)
q_embed = (q * cos) + (rotate_half(q) * sin)
k_embed = (k * cos) + (rotate_half(k) * sin)
return q_embed, k_embed
2D-RoPE
到目前为止,只讨论了RoPE,它是一种一维位置嵌入方法。这对于一维序列很有用,比如文本。
但如果想将RoPE用于二维序列(例如图像),该怎么办呢?
二维RoPE(2D-RoPE)是RoPE的一种简单扩展,在这种方法中,每个输入向量都有一个二维位置:
回到时钟的类比,一个简单的思路是使用两个时钟,一个对应y轴,另一个对应x轴。
为了说明这一点,将初始示例的维度n_dim加倍,这样就有n_dim = 8:
现在它被表示为4对二维向量:
其思路是将该向量进一步拆分为两部分,一部分对应y轴,另一部分对应x轴。
假设这4个向量的位置列表如下:
- [0, 0]
- [0, 1]
- [1, 2]
- [1, 3]
使用一组40°和20°的角度值,可以像这样独立旋转每个部分的向量:
据所知,这种二维旋转位置编码(2D-RoPE)方法被用于Llama 4模型的视觉编码器中。
2D-RoPE with interleaved frequency
在之前的示例中,对两个轴使用了相同的角度值(40°和20°)。但如果想为每个轴使用不同的频率呢?
以Mistral的Pixtral模型为例:
- 首先为所有二维向量对创建一个频率列表,例如:40°、30°、20°、10°
- 然后将这些频率按轴交错分配,这样y轴得到40°、20°,x轴得到30°、10°。
巧妙之处在于,无需先构建一个频率列表(例如40°、30°、20°、10°),再从中挑选奇数位或偶数位的频率值,只需调整n_dim参数值和频率的缩放比例,就能得到相同的结果。可以查看这个PR(拉取请求)了解的具体实现方式。
M-RoPE
Qwen2VL的M-RoPE
M-RoPE是Multimodal-RoPE(多模态旋转位置编码)的缩写,最初由Qwen2VL模型提出。
M-RoPE扩展了二维旋转位置编码(2D-RoPE)的理念,不过现在每个位置包含的维度不止2个。例如,可以有三维[时间、y轴、x轴],甚至更多维度。
其核心思想是,不再将嵌入向量拆分为2部分,而是拆分为……没错,拆分为n部分,其中n是每个位置的维度数量。
如果仔细查看Qwen2VL的config.json文件,会看到一个名为mrope_section的配置,其中包含3个数值。每个数值代表每个部分的二维对数量。
"rope_scaling": {
"type": "mrope",
"mrope_section": [
16,
24,
24
]
},
为了便于理解,举一个简单的例子:当嵌入向量的维度n_dim=8
时,最终会得到4对二维向量:
假设的mrope_section配置为[1,1,2],可以将嵌入向量拆分为3个部分:
然后,使用与二维旋转位置编码(2D-RoPE)中所解释的相同方法,对每个部分独立应用旋转位置编码(RoPE)。
参考文献
- RoFormer: Enhanced Transformer with Rotary Position Embedding,https://arxiv.org/abs/2104.09864
- Qwen2-VL: Enhancing Vision-Language Model’s Perception of the World at Any Resolution,https://arxiv.org/pdf/2409.12191
本文转载自大模型自然语言处理 作者:llmnlp
