Fused Rotary Position Encoding (RoPE) 全方位解析
基于 4 个真实 PaddlePaddle/FastDeploy CUDA Kernel 源码,深度对比 GPT-J (Interleaved)、GPT-NeoX (Half-Split)、Partial Rotary、3D RoPE 等多种实现方式,涵盖数学原理、内存布局、CUDA 实现细节与性能特征。
- RoPE 的核心运算是对 head 内的元素对施加 2D 旋转:
x' = x*cos - y*sin, y' = y*cos + x*sin,不同变体的区别仅在于如何选取 (x, y) 对 - GPT-J (Interleaved) 取相邻元素
(arr[2i], arr[2i+1])配对;GPT-NeoX (Half-Split) 取前半后半(arr[i], arr[i+d/2])配对 — 数学等价,内存布局不同 - Partial Rotary 只旋转 head 的前
rotary_dim个元素,其余保持不动(Phi-3, GLM-4 使用此策略) - 所有频率缩放变体(PI / NTK / YaRN / LongRoPE)只改变预计算的 cos/sin cache,kernel 旋转逻辑完全不变
- Fused Kernel 通常将 QKV split + RoPE + RMSNorm 合并为一个 kernel,减少 global memory 读写次数,提升 memory-bound 场景下的性能
背景 / Background
Rotary Position Embedding (RoPE) 由 Su et al. 在 2021 年的 RoFormer 论文中提出,已成为现代 LLM 的事实标准位置编码方案。LLaMA、Qwen、Baichuan、Mistral、Gemma、Phi、GLM 等主流模型均采用 RoPE 的某种变体。
与绝对位置编码 (APE) 直接加到 token embedding 不同,RoPE 在 attention 计算前对 Query 和 Key 向量施加位置相关的旋转变换。这种设计使得两个 token 的 attention score 只依赖它们的相对位置,天然支持位置外推。
RoPE 数学原理
对于 head 维度 d 中的第 i 对元素,RoPE 定义旋转角度为:
为什么有多种变体
RoPE 的数学原理只定义了"对 d/2 对元素各施加一个旋转",但没有规定这些"对"如何从 d 维向量中选取。这就是不同 RoPE 变体的根本分歧点:
(Interleaved / Half-Split)
(rotary_dim < head_dim)
(时空位置编码)
(PI/NTK/YaRN/...)
四大 RoPE 配对方式详解
1. GPT-J 模式 (Interleaved / 交错配对)
GPT-J 风格将相邻的两个元素视为一对:(arr[2i], arr[2i+1]) 构成第 i 个旋转对。这是 RoPE 原始论文中最自然的实现方式。
内存布局 (head_dim=8, rot_dim=8)
配对关系:(x0,y0), (x1,y1), (x2,y2), (x3,y3) — 每对共享同一个 θ
索引公式
对应 Kernel 代码 (apply_token_rotary_embedding_kernel, IS_NEOX=false)
// GPT-J: 相邻元素配对
x_index = 2 * rot_offset;
y_index = 2 * rot_offset + 1;
cos = cos_ptr[x_index / 2];
sin = sin_ptr[x_index / 2];
const T x = arr[x_index];
const T y = arr[y_index];
arr[x_index] = x * cos - y * sin;
arr[y_index] = y * cos + x * sin;CUDA
代表模型
GPT-J, CodeGen, GPT-NeoX (early), 部分 Falcon 变体
GQAVariableLengthRotarySplitKernel 中,VecSize=4 的向量化 load 一次取 4 个元素 = 2 个旋转对。用 HalfVecSize = VecSize/2 加载 cos/sin,每个 cos/sin 值对应 src_vec[2*i] 和 src_vec[2*i+1]。这意味着一次向量化 load 可以完成 2 个旋转,非常高效。
2. GPT-NeoX 模式 (Half-Split / 前后半配对)
GPT-NeoX 风格将 head 维度切成前半和后半,前半的第 i 个元素与后半的第 i 个元素配对:(arr[i], arr[i + d/2])。
内存布局 (head_dim=8, rot_dim=8)
前半 [0, d/2) 是所有 x,后半 [d/2, d) 是所有 y — 配对关系跨越半个 head
索引公式
对应 Kernel 代码 (apply_token_rotary_embedding_kernel, IS_NEOX=true)
// NeoX: 前半后半配对
x_index = rot_offset;
y_index = embed_dim + rot_offset;
cos = cos_ptr[x_index];
sin = sin_ptr[x_index];
const T x = arr[x_index];
const T y = arr[y_index];
arr[x_index] = x * cos - y * sin;
arr[y_index] = y * cos + x * sin;CUDA
对应 Kernel 代码 (FusedNeoxRopeEmbeddingKernel)
// Fused 版本:从 packed QKV 中拆分并旋转
const int base_idx_left = ... + h_bias; // 前半
const int base_idx_right = base_idx_left + half_lastdim; // 后半
Load<T, VecSize>(&qkv[base_idx_left], &left_vec);
Load<T, VecSize>(&qkv[base_idx_right], &right_vec);
// 旋转:left=x, right=y
left_vec[i] = input_left * cos_tmp - input_right * sin_tmp;
right_vec[i] = input_right * cos_tmp + input_left * sin_tmp;CUDA
代表模型
LLaMA 1/2/3, Qwen, Baichuan, Mistral, Gemma, Yi, DeepSeek
d/2 个元素。一次向量化 load 只能取到 x 或 y 的连续块,需要两次 load 才能凑齐一个旋转对。相比 Interleaved 模式的一次 load 处理两对,NeoX 多了一次全局内存读取。
3. Partial Rotary (部分旋转)
某些模型只旋转 head 维度的前一部分(rotary_dim < head_dim),后面的维度保持不变。这给模型保留了一部分"不受位置影响"的表示空间。
内存布局 (head_dim=8, rotary_dim=4, NeoX 配对)
前 rotary_dim 个元素参与旋转(NeoX 前后半配对),后 head_dim-rotary_dim 个元素 pass-through
对应 Kernel 代码 (GQAVariableLengthNeoxPartialRotarySplitKernel)
if (hi < q_num_head + kv_num_head) { // 只有 Q 和 K 需要旋转
if (h_bias < rotary_dim) { // 在旋转区域内
if (h_bias < half_rotary_dim) {
// 前半:加载后半作为配对
Load(&qkv[base_idx + half_rotary_dim], &src_vec_right);
// x' = x*cos - y*sin
src_vec[i] = input_left * cos - input_right * sin;
} else {
// 后半:加载前半作为配对
Load(&qkv[base_idx - half_rotary_dim], &src_vec_right);
// y' = y*cos + x*sin
src_vec[i] = input_left * cos + input_right * sin;
}
}
// else: h_bias >= rotary_dim, 不旋转,保持原值
}CUDA
代表模型
Phi-2/3 (partial_rotary_factor=0.5), GLM-4/4.1V, ChatGLM 系列, Gemma (部分层)
rotary_dim = head_dim/2),GLM 系列则使用不同的 rotary_dim 比例。
4. 3D RoPE (多维旋转位置编码)
3D RoPE 将位置从标量扩展到多维坐标 (t, h, w),用于多模态和视频场景。在 GQAVariableLengthRotarySplitKernel 中通过 rope_3d 参数支持:
// 3D RoPE: 每个 batch 有独立的 emb 偏移
int64_t new_emb_idx = rope_3d
? emb_idx + ori_bi * head_dim * max_model_len // 按 batch 偏移
: emb_idx; // 标准 1DCUDA
3D RoPE 的核心思想是将 head 维度再细分为若干组,分别编码时间、高度、宽度三个维度的位置。旋转公式不变,只是 cos/sin cache 的组织方式变为 [batch, temporal_pos * spatial_pos, dim_group]。
代表模型
Qwen-VL, LLaVA-Video, VideoRoPE (ICML 2025), VRoPE
Kernel 实现深度分析
下面对用户提供的 4 个 CUDA Kernel 进行逐一拆解。
Kernel 1: apply_rotary_embedding_kernel (基础版)
[N, H, D]"] --> K["GPU Kernel
1 block = 1 token"] B["Key
[N, KVH, D]"] --> K C["position_ids
[N]"] --> K D["cos_sin_cache
[max_pos, rot_dim]"] --> K K --> E["Query (in-place)
已旋转"] K --> F["Key (in-place)
已旋转"]
| 特征 | 描述 |
|---|---|
| 并行策略 | grid.x = num_tokens; 每 block 处理 1 个 token 的所有 head |
| block 内循环 | stride-loop 遍历 num_heads * embed_dim 个旋转对 |
| 配对模式 | 通过模板参数 IS_NEOX 编译期选择 NeoX/GPT-J |
| Q/K 处理 | 先循环处理 Q 的所有 head,再循环处理 K — 串行两轮 |
| cos/sin 来源 | cos_sin_cache[pos * rot_dim] 直接查表 |
| 输出方式 | in-place 修改 Query 和 Key |
| 向量化 | 无(逐元素标量访问) |
Kernel 2: FusedNeoxRopeEmbeddingKernel (Fused QKV Split + NeoX)
[N, 3*H*D/2]"] --> K["Fused Kernel
VecSize=4"] B["cos_emb (float)"] --> K C["sin_emb (float)"] --> K K --> Q["Q [N,H,D]"] K --> Kout["K [N,H,D]"] K --> V["V [N,H,D]"]
| 特征 | 描述 |
|---|---|
| 核心创新 | QKV Split + RoPE 融合为一个 kernel,减少一次全局内存读写 |
| 配对模式 | NeoX (Half-Split);分别加载 left(前半)和 right(后半) |
| 向量化 | VecSize=4,使用 AlignedVector 做 LDG.128 |
| 精度 | cos/sin 使用 float,数据使用 bf16 → 内部 cast 到 float 计算再 cast 回来 |
| V 处理 | V 不需要旋转,但也被 split 出来(qkv_id == 2 跳过旋转,直接拷贝) |
| 线性映射 | 全局 linear_index → (token, qkv_id, head, h_bias) 的索引分解 |
[token, 3, H, D/2](注意是 D/2 不是 D),且 QKV 拼在 half_lastdim 维度上。这与常见的 [token, 3, H, D] 布局不同。kernel 通过 base_idx_left 和 base_idx_right = left + half_lastdim 来寻址前后半,而输出到独立的 Q/K/V tensor 时使用完整的 head_dim。
Kernel 3: GQAVariableLengthRotarySplitKernel (Interleaved + RMSNorm)
seq_lens_enc/dec"] --> K D["q_norm / k_norm
weights"] --> K K -->|"RoPE"| R["旋转 Q,K"] R -->|"RMSNorm"| N["归一化 Q,K"] N --> Q["Q split"] N --> Ko["K split (写入 KV cache 位置)"] K --> V["V split (写入 KV cache 位置)"]
| 特征 | 描述 |
|---|---|
| 核心创新 | QKV Split + RoPE + RMSNorm 三合一 fused kernel |
| 配对模式 | GPT-J (Interleaved):src_vec[2*i] 和 src_vec[2*i+1] |
| GQA 支持 | q_num_head != kv_num_head,Q head 数多于 KV head 数 |
| 变长序列 | 通过 batch_id_per_token、cu_seqlens_q/k、seq_lens_encoder/decoder 支持变长 batch |
| RMSNorm 融合 | 对旋转后的结果做 per-head RMSNorm(WelfordWarpAllReduce 计算方差),然后乘 norm weight |
| 3D RoPE | 通过 rope_3d 标志支持多模态位置编码 |
| 线程组织 | dim3 block(32, 4):每 warp 处理一个 head,4 个 warp 并行处理 4 个 head |
| KV Cache 写入 | K/V 直接写入 cu_seqlens_k 指定的 cache 位置 |
thread_m2,然后通过 WelfordWarpAllReduce(warp shuffle)归约得到整个 head 的方差 warp_m2。整个过程不需要 shared memory,纯寄存器 + shuffle 实现。
Kernel 4: GQAVariableLengthNeoxPartialRotarySplitKernel (Partial NeoX)
seq_lens_enc/dec"] --> K K -->|"h_bias < rotary_dim"| R["NeoX 旋转"] K -->|"h_bias >= rotary_dim"| P["Pass-through"] R --> Q["Q split"] R --> Ko["K split → KV cache"] P --> Q P --> Ko K --> V["V split → KV cache"]
| 特征 | 描述 |
|---|---|
| 核心创新 | NeoX 配对 + Partial Rotary:只旋转前 rotary_dim 个维度 |
| 配对模式 | NeoX (Half-Split),但只在 [0, rotary_dim) 范围内 |
| 前后半判断 | h_bias < half_rotary_dim 时算 x*cos - y*sin;h_bias ≥ half_rotary_dim 时算 y*cos + x*sin |
| SM90 特性 | 使用 cudaGridDependencySynchronize() 和 cudaTriggerProgrammaticLaunchCompletion() 支持 PDL (Programmatic Dependent Launch) |
| 适用模型 | GLM-4, Phi 系列,以及任何 rotary_dim < head_dim 的架构 |
cudaGridDependencySynchronize() 和 cudaTriggerProgrammaticLaunchCompletion() 是 Hopper (SM90) 引入的 Programmatic Dependent Launch API,允许依赖 kernel 在前驱 kernel 部分 block 完成后就开始执行,而不用等前驱 kernel 全部结束。这对于 LLM 推理中的 kernel chain(如 QKV projection → RoPE → Attention)有显著的 latency 优化效果。
对比分析 / Comparison
配对方式对比
| 维度 | GPT-J (Interleaved) | GPT-NeoX (Half-Split) | Partial Rotary | 3D RoPE |
|---|---|---|---|---|
| 配对方式 | (arr[2i], arr[2i+1]) | (arr[i], arr[i+d/2]) | NeoX 或 GPT-J,但限于 rotary_dim | 按维度组分段编码 |
| 数学等价性 | 两者数学上等价(存在置换矩阵将一种转换为另一种) | 子集旋转 | 扩展到多维坐标 | |
| 1 次 Load 覆盖 | 2 个旋转对 | 1 个半区(需 2 次 Load) | 同 NeoX | 同 NeoX |
| cos/sin cache 大小 | [max_pos, d/2] | [max_pos, d] (拼接) | [max_pos, rot_dim/2] | [batch, max_pos, d/2] |
| 代表模型 | GPT-J, CodeGen | LLaMA, Qwen, Mistral | Phi-3, GLM-4 | Qwen-VL, VideoRoPE |
Kernel 实现对比
| Kernel | 配对 | Fused 操作 | 向量化 | GQA | 变长 | 特殊特性 |
|---|---|---|---|---|---|---|
| apply_rotary _embedding |
NeoX / GPT-J (模板切换) |
仅 RoPE | 无 | 否 | 否 | 最简实现,适合教学 |
| FusedNeox RopeEmbedding |
NeoX | QKV Split + RoPE | VecSize=4 | 否 | 否 | bf16→float 精度提升 |
| GQAVariable RotarySplit |
Interleaved | QKV Split + RoPE + RMSNorm |
VecSize=4 | 是 | 是 | 3D RoPE, Warp RMSNorm, KV Cache 直写 |
| GQANeox PartialRotary |
NeoX (Partial) | QKV Split + RoPE | VecSize=4 | 是 | 是 | SM90 PDL, rotary_dim < head_dim |
性能特征对比
Fused 程度 (越高越好)
内存效率 (减少 Global Memory 读写)
频率缩放方法 (影响 cos/sin cache,不影响 kernel)
以下方法只改变 cos_sin_cache 的预计算方式,不需要修改旋转 kernel 本身:
| 方法 | 原理 | 代表模型 | 支持长度 |
|---|---|---|---|
| 调大 θbase | 直接增大 RoPE 基频,如 10000 → 500000 | LLaMA 3 (θ=500K) | 8K → 128K+ |
| Position Interpolation (PI) | 线性压缩 position:pos' = pos / scale |
CodeLlama-16K | 4K → 16K |
| NTK-Aware | 非均匀缩放频率:高频少缩,低频多缩 | 社区推理框架 | 4K → 32K+ |
| Dynamic NTK | 根据实际推理长度动态调整 θbase | 即插即用 | 自适应 |
| YaRN | 分三段处理(高频不动、中频 NTK、低频线性)+ attention 温度缩放 | Nous 系列 | 4K → 128K+ |
| LongRoPE / LongRoPE2 | 搜索每个维度的最优缩放因子(非均匀) | Phi-3-128K | 4K → 2048K |
| Llama 3.1 分段策略 | 按频率分高/中/低三段,高频不缩、低频线性、中间平滑过渡 | Llama 3.1 | 8K → 128K |
cos_sin_cache[pos * rot_dim + dim_offset],就能无修改地支持所有缩放方法。kernel 开发者不需要关心 θ 是怎么算出来的。
结论与建议 / Conclusions
从 4 个真实 kernel 的分析中,可以总结出以下工程建议:
1. 配对方式选择跟随模型架构。 NeoX (Half-Split) 是当前主流(LLaMA 系列),Interleaved 在部分模型中使用。两者数学等价,但内存访问模式不同。不要混用。
2. Fused Kernel 是 LLM 推理优化的标准做法。 将 QKV Split + RoPE + RMSNorm 等操作融合为一个 kernel,可以将 global memory 读写次数从 N 次降为 1 次读 + 1 次写,这对 memory-bound 的 RoPE 操作收益显著。
3. 向量化 Load/Store 必不可少。 基础版 kernel(逐元素访问)与 VecSize=4 版本相比,带宽利用率差距巨大。LDG.128(float4 / 4×bf16)是标准配置。
4. Warp-level 编程是 Fused Kernel 的关键技巧。 如 Kernel 3 中用 WelfordWarpAllReduce 做 warp 内归约计算 RMSNorm,不需要 shared memory 同步,性能和代码复杂度都优于 block-level 方案。
5. 关注 SM90+ 新特性。 Kernel 4 使用的 PDL (Programmatic Dependent Launch) 可以减少 kernel chain 的 pipeline bubble,对 decode latency 敏感的场景值得采用。
6. 频率缩放方法不影响 kernel。 所有 PI / NTK / YaRN / LongRoPE 等长度外推方法只改变 cos/sin cache 的预计算,kernel 本身无需任何修改即可支持。
参考资料 / References
- RoFormer: Enhanced Transformer with Rotary Position Embedding (Su et al., 2021) — RoPE 原始论文
- Rotary Embeddings: A Relative Revolution (EleutherAI Blog) — GPT-NeoX/GPT-J RoPE 实现差异
- Extending the RoPE (EleutherAI Blog, YaRN) — YaRN 与频率缩放方法综述
- YaRN: Efficient Context Window Extension (Peng et al., 2023) — YaRN 论文
- LongRoPE: Extending LLM Context to 2048K (Microsoft, ICML 2024) — 非均匀维度缩放
- LongRoPE2: Near-Lossless LLM Context Window Scaling (2025) — LongRoPE 改进版
- HuggingFace Transformers Issue #25199 — LLaMA RoPE 实现差异讨论
- vLLM Issue #747 — GPT-J vs GPT-NeoX RoPE 实现对比
- vLLM PR #22593 — Partial Rotary Embedding MRoPE Triton 支持
- VideoRoPE: What Makes for Good Video Rotary Position Embedding? (2025) — 3D RoPE 在视频 LLM 中的应用
- Inside RoPE: Rotary Magic into Position Embeddings — 图解 RoPE 原理
- CUDA C++ Programming Guide (NVIDIA) — grid 维度上限与 SM90 PDL 参考