通俗理解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_embed2D-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

















